baychat 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/args.js +15 -0
- package/dist/doctor.js +21 -2
- package/dist/index.js +7 -0
- package/dist/relay/adapters.js +21 -3
- package/dist/relay/commands.js +284 -26
- package/dist/relay/daemon.js +317 -43
- package/dist/relay/held.js +184 -0
- package/dist/relay/mailbox.js +1 -0
- package/dist/relay/owner-pid.js +129 -0
- package/dist/relay/registry.js +11 -0
- package/dist/relay/socket.js +48 -0
- package/dist/runtime-binary.js +14 -0
- package/dist/runtimes.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.procTable = void 0;
|
|
37
|
+
exports.ownerPidFor = ownerPidFor;
|
|
38
|
+
exports.isRuntimeCommand = isRuntimeCommand;
|
|
39
|
+
exports.currentOwnerPid = currentOwnerPid;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
/** How far to walk before giving up. Deep enough for shell wrappers, bounded so a cycle cannot hang an attach. */
|
|
43
|
+
const MAX_HOPS = 12;
|
|
44
|
+
/**
|
|
45
|
+
* The nearest ancestor that IS this runtime, or undefined when none is found.
|
|
46
|
+
*
|
|
47
|
+
* Undefined rather than a fallback to `ppid` on purpose: a witness that lies is
|
|
48
|
+
* worse than no witness. With none, the daemon keeps its previous behaviour and
|
|
49
|
+
* may spawn headlessly — the old bug. With a wrong one it spawns headlessly
|
|
50
|
+
* while the session is alive — the worse bug, and a silent one.
|
|
51
|
+
*/
|
|
52
|
+
function ownerPidFor(runtime, startPid, table) {
|
|
53
|
+
let pid = startPid;
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (let hop = 0; hop < MAX_HOPS; hop++) {
|
|
56
|
+
if (pid <= 1 || seen.has(pid))
|
|
57
|
+
return undefined;
|
|
58
|
+
seen.add(pid);
|
|
59
|
+
if (isRuntimeCommand(table.commandOf(pid), runtime))
|
|
60
|
+
return pid;
|
|
61
|
+
const parent = table.parentOf(pid);
|
|
62
|
+
if (parent === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
pid = parent;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether a command line IS the runtime, rather than merely mentioning it.
|
|
70
|
+
*
|
|
71
|
+
* Only the first two tokens are considered — the executable, and the script when
|
|
72
|
+
* that executable is an interpreter. The attach process itself runs
|
|
73
|
+
* `node …/baychat relay attach --runtime claude`, which CONTAINS "claude" as an
|
|
74
|
+
* argument; matching anywhere on the line would make every attach its own owner
|
|
75
|
+
* and defeat the check entirely.
|
|
76
|
+
*/
|
|
77
|
+
function isRuntimeCommand(command, runtime) {
|
|
78
|
+
if (!command)
|
|
79
|
+
return false;
|
|
80
|
+
const tokens = command.trim().split(/\s+/).filter(Boolean);
|
|
81
|
+
if (tokens.length === 0)
|
|
82
|
+
return false;
|
|
83
|
+
if (namesRuntime(tokens[0], runtime))
|
|
84
|
+
return true;
|
|
85
|
+
// An interpreter's SCRIPT is the real executable: `node …/codex/bin/codex.js`
|
|
86
|
+
// is Codex. Only consulted behind an interpreter, and only for something that
|
|
87
|
+
// looks like a path — otherwise `grep claude syslog` would match its own
|
|
88
|
+
// search term, and every attach would match its own `--runtime claude`.
|
|
89
|
+
if (!INTERPRETERS.has(path.basename(tokens[0]).toLowerCase()))
|
|
90
|
+
return false;
|
|
91
|
+
const script = tokens[1];
|
|
92
|
+
if (!script || !/[\\/]/.test(script))
|
|
93
|
+
return false;
|
|
94
|
+
return namesRuntime(script, runtime);
|
|
95
|
+
}
|
|
96
|
+
const INTERPRETERS = new Set(["node", "node.exe", "bun", "deno", "python", "python3"]);
|
|
97
|
+
/** `claude`, `claude.exe`, `codex.js` — the runtime's binary, however spelled. Not `claudette`. */
|
|
98
|
+
function namesRuntime(token, runtime) {
|
|
99
|
+
const base = path.basename(token).toLowerCase();
|
|
100
|
+
return base === runtime || base.startsWith(`${runtime}.`);
|
|
101
|
+
}
|
|
102
|
+
/** Reads the live process tree from /proc. Returns nothing where /proc is absent (macOS, Windows). */
|
|
103
|
+
exports.procTable = {
|
|
104
|
+
parentOf(pid) {
|
|
105
|
+
try {
|
|
106
|
+
// Field 4 of /proc/<pid>/stat is ppid. Read past the comm field, which is
|
|
107
|
+
// parenthesised and may itself contain spaces.
|
|
108
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
109
|
+
const after = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/);
|
|
110
|
+
const ppid = Number(after[1]);
|
|
111
|
+
return Number.isFinite(ppid) ? ppid : undefined;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
commandOf(pid) {
|
|
118
|
+
try {
|
|
119
|
+
return fs.readFileSync(`/proc/${pid}/cmdline`, "utf8").replace(/\0/g, " ").trim() || undefined;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
/** The owning runtime process for THIS attach, read from the live tree. */
|
|
127
|
+
function currentOwnerPid(runtime) {
|
|
128
|
+
return ownerPidFor(runtime, process.ppid, exports.procTable);
|
|
129
|
+
}
|
package/dist/relay/registry.js
CHANGED
|
@@ -121,6 +121,17 @@ class SessionRegistry {
|
|
|
121
121
|
if (t)
|
|
122
122
|
t.attached = attached;
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Record that a delivery to this session actually landed.
|
|
126
|
+
*
|
|
127
|
+
* Separate from `setAttached` because it means something stronger: attached is
|
|
128
|
+
* a state that can be claimed, this is an event that was observed.
|
|
129
|
+
*/
|
|
130
|
+
markDelivered(name, at) {
|
|
131
|
+
const t = this.sessions.get(name);
|
|
132
|
+
if (t)
|
|
133
|
+
t.lastDeliveredAt = at;
|
|
134
|
+
}
|
|
124
135
|
/**
|
|
125
136
|
* Drop targets whose session is no longer live server-side.
|
|
126
137
|
*
|
package/dist/relay/socket.js
CHANGED
|
@@ -39,6 +39,7 @@ exports.isNamedPipe = isNamedPipe;
|
|
|
39
39
|
exports.pidFilePath = pidFilePath;
|
|
40
40
|
exports.createFrameReader = createFrameReader;
|
|
41
41
|
exports.writeFrame = writeFrame;
|
|
42
|
+
exports.writeFrameAck = writeFrameAck;
|
|
42
43
|
exports.probeSocketDetailed = probeSocketDetailed;
|
|
43
44
|
exports.probeSocket = probeSocket;
|
|
44
45
|
exports.describeProbeFailure = describeProbeFailure;
|
|
@@ -156,6 +157,53 @@ function createFrameReader(onFrame, onBad) {
|
|
|
156
157
|
function writeFrame(sock, frame) {
|
|
157
158
|
sock.write(JSON.stringify(frame) + "\n");
|
|
158
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Write a frame and WAIT for the socket to take it, surfacing the failure if it
|
|
162
|
+
* does not.
|
|
163
|
+
*
|
|
164
|
+
* `writeFrame` is fire-and-forget. `sock.write()` buffers and returns, so a peer
|
|
165
|
+
* that has already gone produces an EPIPE on the socket's `error` event LATER —
|
|
166
|
+
* by which time the caller has recorded `woken`. That is a lost message filed as
|
|
167
|
+
* delivered, which is the single outcome `relay status` exists to make
|
|
168
|
+
* impossible: every other failure is at least visible as pending.
|
|
169
|
+
*
|
|
170
|
+
* Three ways this settles, and all three are needed. The write callback covers
|
|
171
|
+
* the ordinary case; the `error` event covers an EPIPE that arrives without one;
|
|
172
|
+
* `close` covers a peer that vanishes while the bytes are still buffered, which
|
|
173
|
+
* reports no error at all.
|
|
174
|
+
*
|
|
175
|
+
* HONEST LIMIT — this is a FLUSH confirmation, not a receiver acknowledgement.
|
|
176
|
+
* It proves the bytes left this process, not that the agent read them. A true
|
|
177
|
+
* end-to-end ack needs an `ack` frame back from the attach client, and every
|
|
178
|
+
* already-installed client would have to be taught to send one before the
|
|
179
|
+
* daemon could require it. What this closes is the failure that was actually
|
|
180
|
+
* observed — a dead peer recorded as woken — not the whole class.
|
|
181
|
+
*/
|
|
182
|
+
function writeFrameAck(sock, frame) {
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
if (sock.destroyed || sock.writableEnded) {
|
|
185
|
+
reject(new Error("the attach socket is already closed"));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
let settled = false;
|
|
189
|
+
const settle = (err) => {
|
|
190
|
+
if (settled)
|
|
191
|
+
return;
|
|
192
|
+
settled = true;
|
|
193
|
+
sock.off("error", onError);
|
|
194
|
+
sock.off("close", onClose);
|
|
195
|
+
if (err)
|
|
196
|
+
reject(err);
|
|
197
|
+
else
|
|
198
|
+
resolve();
|
|
199
|
+
};
|
|
200
|
+
const onError = (err) => settle(err);
|
|
201
|
+
const onClose = () => settle(new Error("the attach socket closed before the frame was flushed"));
|
|
202
|
+
sock.once("error", onError);
|
|
203
|
+
sock.once("close", onClose);
|
|
204
|
+
sock.write(JSON.stringify(frame) + "\n", (err) => settle(err ?? undefined));
|
|
205
|
+
});
|
|
206
|
+
}
|
|
159
207
|
/**
|
|
160
208
|
* Probe the attach endpoint, KEEPING THE REASON IT FAILED.
|
|
161
209
|
*
|
package/dist/runtime-binary.js
CHANGED
|
@@ -173,6 +173,20 @@ function joinPath(dir, file, platform) {
|
|
|
173
173
|
* @returns file + prefix args to spawn it with, always `shell: false`
|
|
174
174
|
*/
|
|
175
175
|
function spawnPlanFor(candidate, platform) {
|
|
176
|
+
// A SCRIPT NEEDS ITS INTERPRETER, on every platform.
|
|
177
|
+
//
|
|
178
|
+
// The managed Codex fallback records `<CODEX_MANAGED_PACKAGE_ROOT>/bin/codex.js`
|
|
179
|
+
// — the file the npm install is actually running — and a `.js` cannot be
|
|
180
|
+
// spawned directly. On Unix that needs a shebang AND the execute bit, neither
|
|
181
|
+
// of which an npm package guarantees; on Windows there is no execute bit at
|
|
182
|
+
// all, and only `.cmd`/`.bat` were wrapped. So the recorded path was one the
|
|
183
|
+
// daemon could not run, which is the 2026-08-30 ENOENT failure in a new place.
|
|
184
|
+
//
|
|
185
|
+
// `process.execPath` is the Node running this CLI. Deliberately NOT a bare
|
|
186
|
+
// "node": the daemon's PATH may not have one — that is the whole reason
|
|
187
|
+
// `runtimeBin` is recorded by the session in the first place.
|
|
188
|
+
if (/\.(mjs|cjs|js)$/i.test(candidate))
|
|
189
|
+
return { file: process.execPath, prefixArgs: [candidate] };
|
|
176
190
|
const needsInterpreter = platform === "win32" && /\.(cmd|bat)$/i.test(candidate);
|
|
177
191
|
if (!needsInterpreter)
|
|
178
192
|
return { file: candidate, prefixArgs: [] };
|
package/dist/runtimes.js
CHANGED
|
@@ -133,6 +133,14 @@ before ending your turn. Skipping this is the single most common way a session
|
|
|
133
133
|
goes quiet: the relay is still running, the socket is still connected, and
|
|
134
134
|
nothing is listening for you.
|
|
135
135
|
|
|
136
|
+
**And after every INTERRUPT.** Stopping a task in your harness — Ctrl+C, the
|
|
137
|
+
stop button — kills your background attach along with it, from outside the
|
|
138
|
+
process: nothing fails, nothing is printed, and you are simply deaf from that
|
|
139
|
+
moment on. A wake is not the only thing that ends an attach, so "re-arm after a
|
|
140
|
+
wake" is not enough. **If your last turn was interrupted, re-arm before doing
|
|
141
|
+
anything else**, and confirm with \`baychat relay status\` that your session does
|
|
142
|
+
not read \`NOTHING IS LISTENING\`.
|
|
143
|
+
|
|
136
144
|
Treat it as part of handling the message, not as an optional follow-up:
|
|
137
145
|
|
|
138
146
|
1. attach exits with a message
|