@nexrall/code-core 1.4.59 → 1.4.60
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/agent/crossProcessLock.d.ts +45 -0
- package/dist/agent/crossProcessLock.d.ts.map +1 -0
- package/dist/agent/crossProcessLock.js +270 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +114 -16
- package/dist/agent/peerRegistry.d.ts +64 -0
- package/dist/agent/peerRegistry.d.ts.map +1 -0
- package/dist/agent/peerRegistry.js +238 -0
- package/dist/agent/peerTransport.d.ts +122 -0
- package/dist/agent/peerTransport.d.ts.map +1 -0
- package/dist/agent/peerTransport.js +348 -0
- package/dist/agent/sharedTasks.d.ts +63 -0
- package/dist/agent/sharedTasks.d.ts.map +1 -0
- package/dist/agent/sharedTasks.js +284 -0
- package/dist/agent/worktree.d.ts +111 -0
- package/dist/agent/worktree.d.ts.map +1 -0
- package/dist/agent/worktree.js +502 -0
- package/dist/agent/worktreeEnforcement.d.ts +29 -0
- package/dist/agent/worktreeEnforcement.d.ts.map +1 -0
- package/dist/agent/worktreeEnforcement.js +203 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +5 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/tools/executor.d.ts +4 -1
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +194 -4
- package/dist/types.d.ts +83 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,348 @@
|
|
|
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._peerTransportInternal = void 0;
|
|
37
|
+
exports.socketPathFor = socketPathFor;
|
|
38
|
+
exports.startPeerInbox = startPeerInbox;
|
|
39
|
+
exports.sendPeerMessage = sendPeerMessage;
|
|
40
|
+
exports.broadcastPeerMessage = broadcastPeerMessage;
|
|
41
|
+
/**
|
|
42
|
+
* Peer transport — the actual message delivery between two independent
|
|
43
|
+
* sessions on the same machine, once peerRegistry.ts has let them discover
|
|
44
|
+
* each other. Mirrors Claude Code's cross-session messaging transport:
|
|
45
|
+
* same-machine delivery goes over a private Unix domain socket per session,
|
|
46
|
+
* never through any server — "Message content is not sent to Anthropic
|
|
47
|
+
* unless the sessions are on different machines."
|
|
48
|
+
*
|
|
49
|
+
* ONE ENDPOINT PER SESSION, not a shared broker: `~/.nexrall/peers/<id>.sock`
|
|
50
|
+
* on macOS/Linux (mode 0600, owner-only), `\\.\pipe\nexrall-peer-<id>` on
|
|
51
|
+
* Windows (see socketPathFor's own doc comment for why the two need
|
|
52
|
+
* different safety-check strategies). A sender connects directly to the
|
|
53
|
+
* RECIPIENT's endpoint, writes one newline-terminated JSON message, and
|
|
54
|
+
* disconnects — no persistent connection, no broker process to keep alive
|
|
55
|
+
* or crash-recover.
|
|
56
|
+
*
|
|
57
|
+
* SECURITY — this is genuinely adversarial territory, not decoration
|
|
58
|
+
* -------------------------------------------------------------------
|
|
59
|
+
* A peer message is the OUTPUT OF ANOTHER LLM CALL, running under whatever
|
|
60
|
+
* prompted THAT session — treat it as untrusted exactly like fetched web
|
|
61
|
+
* content (see this repo's own prompt-injection-defense conventions). This
|
|
62
|
+
* module's job is to make the TRANSPORT itself hard to abuse; the CONTENT
|
|
63
|
+
* still has to be handled as untrusted data by whoever renders/acts on it
|
|
64
|
+
* (see loop.ts's renderPeerMessage, once that's wired in Priority 4c/4d):
|
|
65
|
+
* - Socket file must not be a SYMLINK (planting a symlink at the expected
|
|
66
|
+
* socket path pointing somewhere else is the exact class of bypass
|
|
67
|
+
* worktreeEnforcement.ts's git-redirect checks guard against for a
|
|
68
|
+
* different resource — same principle, applied here to sockets).
|
|
69
|
+
* - Socket file must be owned by the SAME uid as the connecting process
|
|
70
|
+
* (`fs.statSync(...).uid === process.getuid()`) — refuses connecting to
|
|
71
|
+
* a socket some other user on a shared machine planted at a guessed path.
|
|
72
|
+
* - Message size is capped (MAX_MESSAGE_BYTES) so one peer can't wedge
|
|
73
|
+
* another's inbox or blow its context window with a single message.
|
|
74
|
+
* - A message NEVER carries conversation history or file contents —
|
|
75
|
+
* plain text only, matching Claude Code's own "no context, just
|
|
76
|
+
* text" design. Context-sharing is what `--resume`/session resume is
|
|
77
|
+
* for, not this channel.
|
|
78
|
+
* - This module does not interpret message text as a command — no
|
|
79
|
+
* slash-command parsing, no execution. It is the receiving session's
|
|
80
|
+
* `renderPeerMessage` (loop.ts side, per this module's own doc header
|
|
81
|
+
* rationale) that must keep it fenced as untrusted data, never as an
|
|
82
|
+
* instruction with the same authority as the actual user.
|
|
83
|
+
*/
|
|
84
|
+
const fs = __importStar(require("fs"));
|
|
85
|
+
const os = __importStar(require("os"));
|
|
86
|
+
const path = __importStar(require("path"));
|
|
87
|
+
const net = __importStar(require("net"));
|
|
88
|
+
const MAX_MESSAGE_BYTES = 8 * 1024; // 8 KB — a coordination ping, not a payload channel
|
|
89
|
+
const CONNECT_TIMEOUT_MS = 3000;
|
|
90
|
+
const isWindows = process.platform === 'win32';
|
|
91
|
+
// `struct sockaddr_un`'s `sun_path` field — what a Unix domain socket path
|
|
92
|
+
// actually gets copied into at the OS level — is a FIXED-SIZE buffer: 104
|
|
93
|
+
// bytes on macOS/BSD, 108 on Linux. A path longer than that makes
|
|
94
|
+
// `listen()`/`connect()` fail with EINVAL, and — this is the real bug this
|
|
95
|
+
// constant exists to prevent, caught by a genuinely failing test, not
|
|
96
|
+
// invented defensively — Node's `net.Server` reports that failure ONLY via
|
|
97
|
+
// its `'error'` event, which this module's `startPeerInbox` used to just
|
|
98
|
+
// swallow (`server.on('error', () => {/* best-effort */})`), so the peer
|
|
99
|
+
// would look successfully registered (a socketPath string was returned)
|
|
100
|
+
// while silently never being able to receive a single message. A deep home
|
|
101
|
+
// directory (`~/.nexrall/peers/<16-hex-char-id>.sock`) is enough to exceed
|
|
102
|
+
// this in practice, not just in a test harness using a long os.tmpdir()
|
|
103
|
+
// path — so this is a real production risk, not merely a test artifact.
|
|
104
|
+
// Kept conservatively below the tighter of the two OS limits.
|
|
105
|
+
const MAX_SOCKET_PATH_LENGTH = 100;
|
|
106
|
+
function preferredSocketDir() {
|
|
107
|
+
return process.env.NEXRALL_PEERS_DIR || path.join(os.homedir(), '.nexrall', 'peers');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Fallback used ONLY when preferredSocketDir()'s natural path would exceed
|
|
111
|
+
* MAX_SOCKET_PATH_LENGTH. `/tmp` is short and present on essentially every
|
|
112
|
+
* POSIX system — the same fallback other Unix-socket-using tools facing
|
|
113
|
+
* this identical `sockaddr_un` constraint use (tmux, docker compose, etc).
|
|
114
|
+
* Peer ids are random enough (16 hex chars, minted by registerPeer) that
|
|
115
|
+
* collisions between unrelated sessions sharing this flat fallback
|
|
116
|
+
* namespace are not a practical concern.
|
|
117
|
+
*/
|
|
118
|
+
function fallbackSocketDir() {
|
|
119
|
+
return '/tmp';
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Where this peer's transport endpoint lives.
|
|
123
|
+
*
|
|
124
|
+
* macOS/Linux: a real file at `~/.nexrall/peers/<id>.sock` — a Unix domain
|
|
125
|
+
* socket, backed by an actual filesystem entry `verifySocketSafety` can
|
|
126
|
+
* `lstat` and validate — UNLESS that path would exceed
|
|
127
|
+
* MAX_SOCKET_PATH_LENGTH, in which case fallbackSocketDir() is used instead
|
|
128
|
+
* (see its own doc comment).
|
|
129
|
+
*
|
|
130
|
+
* Windows: `\\.\pipe\nexrall-peer-<id>` — a NAMED PIPE, which lives in the
|
|
131
|
+
* NT kernel object namespace, NOT the regular NTFS filesystem tree. There is
|
|
132
|
+
* no directory to create and no on-disk file for `fs.lstatSync` to inspect
|
|
133
|
+
* (see verifySocketSafety's Windows branch for what this means for the
|
|
134
|
+
* safety checks), and no `sockaddr_un`-style length limit either — Windows
|
|
135
|
+
* named pipe paths have a much larger practical limit, so no fallback logic
|
|
136
|
+
* applies there. `net.createServer().listen(path)` / `net.createConnection
|
|
137
|
+
* (path)` both accept this exact path form natively — no separate Windows
|
|
138
|
+
* API surface needed, Node's own `net` module dispatches to named pipes
|
|
139
|
+
* automatically when the path matches this pattern.
|
|
140
|
+
*/
|
|
141
|
+
function socketPathFor(peerId) {
|
|
142
|
+
if (isWindows)
|
|
143
|
+
return `\\\\.\\pipe\\nexrall-peer-${peerId}`;
|
|
144
|
+
const preferred = path.join(preferredSocketDir(), `${peerId}.sock`);
|
|
145
|
+
if (preferred.length <= MAX_SOCKET_PATH_LENGTH)
|
|
146
|
+
return preferred;
|
|
147
|
+
return path.join(fallbackSocketDir(), `nexrall-peer-${peerId}.sock`);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Validate that `socketPath` is safe to connect to BEFORE connecting —
|
|
151
|
+
* see this module's header for exactly which bypass this closes. Returns an
|
|
152
|
+
* error string on failure, null when safe to proceed.
|
|
153
|
+
*
|
|
154
|
+
* WINDOWS CAVEAT: named pipes are kernel objects, not filesystem entries, so
|
|
155
|
+
* `fs.lstatSync` (what the symlink/uid checks below are built on) does not
|
|
156
|
+
* apply to them — there is no on-disk path component for a symlink to spoof
|
|
157
|
+
* in the first place, so that specific attack class does not carry over.
|
|
158
|
+
* The uid-ownership check has no direct equivalent from plain Node `net`
|
|
159
|
+
* either (that would require reading the pipe's Windows security descriptor
|
|
160
|
+
* via a native call, which this module deliberately does not add). The
|
|
161
|
+
* actual safety boundary on Windows is therefore the OS's own named-pipe
|
|
162
|
+
* ACL (set by whoever created it, defaulting to the creating user + admins)
|
|
163
|
+
* plus the connection attempt itself failing cleanly (ENOENT/EACCES) for a
|
|
164
|
+
* pipe that does not exist or is not accessible — handled by sendPeerMessage's
|
|
165
|
+
* own error path, not by a pre-check here. This is a genuinely weaker
|
|
166
|
+
* guarantee than the Unix path's explicit checks, called out here rather
|
|
167
|
+
* than silently assumed equivalent.
|
|
168
|
+
*/
|
|
169
|
+
function verifySocketSafety(socketPath) {
|
|
170
|
+
if (isWindows)
|
|
171
|
+
return null; // see this function's own doc comment for why
|
|
172
|
+
let st;
|
|
173
|
+
try {
|
|
174
|
+
st = fs.lstatSync(socketPath); // lstat, NOT stat — must inspect the link itself, not what it points to
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return 'socket does not exist (peer may have exited)';
|
|
178
|
+
}
|
|
179
|
+
if (st.isSymbolicLink()) {
|
|
180
|
+
return 'refusing to connect: peer socket path is a symlink (possible spoofing attempt)';
|
|
181
|
+
}
|
|
182
|
+
if (!st.isSocket()) {
|
|
183
|
+
return 'refusing to connect: peer socket path is not actually a socket';
|
|
184
|
+
}
|
|
185
|
+
if (typeof process.getuid === 'function' && st.uid !== process.getuid()) {
|
|
186
|
+
return 'refusing to connect: peer socket is owned by a different user';
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Start listening for incoming peer messages on this session's own
|
|
192
|
+
* socket/pipe. Unix domain socket on macOS/Linux, named pipe on Windows —
|
|
193
|
+
* see socketPathFor's own doc comment for the platform split. Not tested
|
|
194
|
+
* against a real Windows machine (dev/CI for this repo runs macOS/Linux);
|
|
195
|
+
* the implementation follows Node's own documented cross-platform `net`
|
|
196
|
+
* behavior for named-pipe paths, but treat the Windows path as less
|
|
197
|
+
* battle-tested than the Unix one until it has run on real Windows.
|
|
198
|
+
*/
|
|
199
|
+
function startPeerInbox(peerId, opts) {
|
|
200
|
+
const socketPath = socketPathFor(peerId);
|
|
201
|
+
if (!isWindows) {
|
|
202
|
+
fs.mkdirSync(path.dirname(socketPath), { recursive: true });
|
|
203
|
+
// A stale socket file from a previous run of this exact peerId (extremely
|
|
204
|
+
// unlikely — ids are random — but a crash could theoretically leave one)
|
|
205
|
+
// would make `server.listen` fail with EADDRINUSE. Safe to remove
|
|
206
|
+
// unconditionally: peerId is freshly minted per registerPeer() call, so
|
|
207
|
+
// nothing legitimate could still be listening on it.
|
|
208
|
+
try {
|
|
209
|
+
fs.unlinkSync(socketPath);
|
|
210
|
+
}
|
|
211
|
+
catch { /* did not exist */ }
|
|
212
|
+
}
|
|
213
|
+
// No directory/stale-file handling needed on Windows — named pipes have no
|
|
214
|
+
// on-disk path component (see socketPathFor's doc comment), and a stale
|
|
215
|
+
// pipe from a previous instance of this exact random peerId is exactly as
|
|
216
|
+
// vanishingly unlikely there as the Unix case above.
|
|
217
|
+
const server = net.createServer((conn) => {
|
|
218
|
+
let buf = '';
|
|
219
|
+
conn.on('data', (chunk) => {
|
|
220
|
+
buf += chunk.toString('utf-8');
|
|
221
|
+
if (buf.length > MAX_MESSAGE_BYTES * 2) {
|
|
222
|
+
// A sender ignoring MAX_MESSAGE_BYTES (or trying to flood) gets cut
|
|
223
|
+
// off rather than let this session buffer unboundedly.
|
|
224
|
+
conn.destroy();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const nl = buf.indexOf('\n');
|
|
228
|
+
if (nl === -1)
|
|
229
|
+
return;
|
|
230
|
+
const line = buf.slice(0, nl);
|
|
231
|
+
buf = '';
|
|
232
|
+
try {
|
|
233
|
+
const parsed = JSON.parse(line);
|
|
234
|
+
if (typeof parsed.text === 'string' && typeof parsed.from === 'string' && typeof parsed.fromId === 'string') {
|
|
235
|
+
opts.onMessage({
|
|
236
|
+
from: parsed.from,
|
|
237
|
+
fromId: parsed.fromId,
|
|
238
|
+
text: parsed.text.slice(0, MAX_MESSAGE_BYTES),
|
|
239
|
+
sentAt: typeof parsed.sentAt === 'number' ? parsed.sentAt : Date.now(),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch { /* malformed message — drop silently, same as a corrupt network packet */ }
|
|
244
|
+
conn.end();
|
|
245
|
+
});
|
|
246
|
+
conn.on('error', () => { });
|
|
247
|
+
});
|
|
248
|
+
server.listen(socketPath, () => {
|
|
249
|
+
if (isWindows)
|
|
250
|
+
return; // chmod has no meaning for a named pipe — its access control is the OS's own pipe DACL, not POSIX file mode.
|
|
251
|
+
// Owner-only permissions — a message channel is exactly the kind of
|
|
252
|
+
// resource that must not be readable/writable by other users on a
|
|
253
|
+
// shared machine.
|
|
254
|
+
try {
|
|
255
|
+
fs.chmodSync(socketPath, 0o600);
|
|
256
|
+
}
|
|
257
|
+
catch { /* best-effort on platforms without POSIX chmod semantics */ }
|
|
258
|
+
});
|
|
259
|
+
// See PeerInboxOptions.onError's own comment for why this used to be a
|
|
260
|
+
// pure no-op (`() => {}`) and why that was a real bug, not a deliberate
|
|
261
|
+
// simplification: `listen()` failing (EINVAL for an over-length path,
|
|
262
|
+
// EADDRINUSE, EACCES, ...) used to be indistinguishable from success from
|
|
263
|
+
// this function's return value alone.
|
|
264
|
+
server.on('error', (err) => { opts.onError?.(err instanceof Error ? err : new Error(String(err))); });
|
|
265
|
+
return {
|
|
266
|
+
socketPath,
|
|
267
|
+
close() {
|
|
268
|
+
// `server.close()` itself unlinks the Unix socket file it created via
|
|
269
|
+
// `listen(path)` — verified directly (Node does this internally, not
|
|
270
|
+
// merely "documented and trusted"); the explicit unlinkSync below is
|
|
271
|
+
// therefore belt-and-suspenders for the case where close() fires
|
|
272
|
+
// asynchronously and a caller inspects the filesystem before that
|
|
273
|
+
// callback runs, not a correction of a leak. On Windows there is no
|
|
274
|
+
// socket FILE at all (see socketPathFor's doc comment — a named pipe
|
|
275
|
+
// is a kernel object), so this unlink is always a harmless no-op
|
|
276
|
+
// there (ENOENT, swallowed) rather than something that needs its own
|
|
277
|
+
// platform branch.
|
|
278
|
+
server.close();
|
|
279
|
+
try {
|
|
280
|
+
fs.unlinkSync(socketPath);
|
|
281
|
+
}
|
|
282
|
+
catch { /* already gone, or never existed (Windows) */ }
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Send a message to a peer at `targetSocketPath`. Fire-and-forget from the
|
|
288
|
+
* caller's perspective (resolves once the write completes or fails) — there
|
|
289
|
+
* is no delivery receipt, matching Claude Code's own "best effort" framing
|
|
290
|
+
* for cross-session messaging.
|
|
291
|
+
*/
|
|
292
|
+
function sendPeerMessage(targetSocketPath, message) {
|
|
293
|
+
return new Promise((resolve) => {
|
|
294
|
+
const safetyError = verifySocketSafety(targetSocketPath);
|
|
295
|
+
if (safetyError) {
|
|
296
|
+
resolve({ ok: false, error: safetyError });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const payload = { ...message, text: message.text.slice(0, MAX_MESSAGE_BYTES), sentAt: Date.now() };
|
|
300
|
+
const line = JSON.stringify(payload) + '\n';
|
|
301
|
+
if (Buffer.byteLength(line, 'utf-8') > MAX_MESSAGE_BYTES * 2) {
|
|
302
|
+
resolve({ ok: false, error: `message too large (max ${MAX_MESSAGE_BYTES} bytes)` });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const socket = net.createConnection(targetSocketPath);
|
|
306
|
+
const timeout = setTimeout(() => {
|
|
307
|
+
socket.destroy();
|
|
308
|
+
resolve({ ok: false, error: 'timed out connecting to peer — it may be busy or have just exited' });
|
|
309
|
+
}, CONNECT_TIMEOUT_MS);
|
|
310
|
+
socket.on('connect', () => {
|
|
311
|
+
socket.write(line, (err) => {
|
|
312
|
+
clearTimeout(timeout);
|
|
313
|
+
if (err) {
|
|
314
|
+
resolve({ ok: false, error: err.message });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
socket.end();
|
|
318
|
+
resolve({ ok: true });
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
socket.on('error', (err) => {
|
|
322
|
+
clearTimeout(timeout);
|
|
323
|
+
resolve({ ok: false, error: err.message });
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Send the SAME message to every target in `targets`, in parallel. Each
|
|
329
|
+
* target gets its own independent sendPeerMessage call — one unreachable or
|
|
330
|
+
* slow peer cannot block or fail delivery to the others (no partial-failure
|
|
331
|
+
* cascade, no head-of-line blocking on a single slow socket).
|
|
332
|
+
*
|
|
333
|
+
* Deliberately a thin fan-out over the existing single-target
|
|
334
|
+
* sendPeerMessage rather than a new wire-level primitive: broadcast is a
|
|
335
|
+
* CALLER-SIDE concept (send to N addresses), not a transport-level one —
|
|
336
|
+
* there is no multicast socket underneath this, just N ordinary point-to-
|
|
337
|
+
* point sends, which keeps every security check (verifySocketSafety, size
|
|
338
|
+
* caps) shared with the single-message path instead of duplicated.
|
|
339
|
+
*/
|
|
340
|
+
async function broadcastPeerMessage(targets, message) {
|
|
341
|
+
const results = await Promise.all(targets.map(async (t) => {
|
|
342
|
+
const r = await sendPeerMessage(t.socketPath, message);
|
|
343
|
+
return { name: t.name, ok: r.ok, error: r.error };
|
|
344
|
+
}));
|
|
345
|
+
return results;
|
|
346
|
+
}
|
|
347
|
+
exports._peerTransportInternal = { verifySocketSafety, MAX_MESSAGE_BYTES, CONNECT_TIMEOUT_MS, isWindows };
|
|
348
|
+
//# sourceMappingURL=peerTransport.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'blocked';
|
|
2
|
+
export interface SharedTask {
|
|
3
|
+
id: string;
|
|
4
|
+
content: string;
|
|
5
|
+
status: TaskStatus;
|
|
6
|
+
/** Session name/id that currently owns this task (see peer registry, once that lands) — undefined for pending/completed. */
|
|
7
|
+
claimedBy?: string;
|
|
8
|
+
/** pid of the process that claimed it — used for the liveness check, same idea as crossProcessLock's stale-lock detection. */
|
|
9
|
+
claimedByPid?: number;
|
|
10
|
+
/** IDs of OTHER tasks in this same list that must be `completed` before this one is claimable. */
|
|
11
|
+
blockedBy?: string[];
|
|
12
|
+
createdAt: number;
|
|
13
|
+
updatedAt: number;
|
|
14
|
+
claimedAt?: number;
|
|
15
|
+
}
|
|
16
|
+
declare function tasksRoot(): string;
|
|
17
|
+
declare function scopeDir(workDir: string): string;
|
|
18
|
+
declare function taskPath(workDir: string, taskId: string): string;
|
|
19
|
+
/** A claimed task whose owning process is gone, or whose claim has gone stale with no update, is treated as abandoned. */
|
|
20
|
+
declare function isClaimAbandoned(task: SharedTask): boolean;
|
|
21
|
+
/** Whether every task ID in `blockedBy` is `completed` — i.e. this task is actually claimable right now. */
|
|
22
|
+
declare function isUnblocked(workDir: string, task: SharedTask): boolean;
|
|
23
|
+
/** List every task registered for `workDir`, newest first isn't guaranteed — sort by createdAt if display order matters to the caller. */
|
|
24
|
+
export declare function listSharedTasks(workDir: string): SharedTask[];
|
|
25
|
+
/** Create a new pending task. `id` auto-generated (short random) unless the caller supplies a stable one (e.g. re-registering from a static plan). */
|
|
26
|
+
export declare function createSharedTask(workDir: string, content: string, opts?: {
|
|
27
|
+
id?: string;
|
|
28
|
+
blockedBy?: string[];
|
|
29
|
+
}): SharedTask;
|
|
30
|
+
export type ClaimResult = {
|
|
31
|
+
ok: true;
|
|
32
|
+
task: SharedTask;
|
|
33
|
+
} | {
|
|
34
|
+
ok: false;
|
|
35
|
+
reason: 'not-found' | 'already-claimed' | 'blocked' | 'not-pending';
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Atomically claim `taskId` for `claimedBy` (a peer/session name). Race-free
|
|
39
|
+
* against another process calling this at the same instant — see this
|
|
40
|
+
* module's header comment for exactly why.
|
|
41
|
+
*/
|
|
42
|
+
export declare function claimTask(workDir: string, taskId: string, claimedBy: string): Promise<ClaimResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Update a task's status. Used for both normal progress (in_progress →
|
|
45
|
+
* completed) and releasing a claim early (in_progress → pending, e.g. a
|
|
46
|
+
* session that claimed a task then decided not to do it after all). Also
|
|
47
|
+
* lock-guarded — a status update racing a claim attempt must not corrupt
|
|
48
|
+
* either operation's view of the task.
|
|
49
|
+
*/
|
|
50
|
+
export declare function updateSharedTaskStatus(workDir: string, taskId: string, status: TaskStatus, opts?: {
|
|
51
|
+
claimedBy?: string;
|
|
52
|
+
}): Promise<ClaimResult>;
|
|
53
|
+
export declare const _sharedTasksInternal: {
|
|
54
|
+
tasksRoot: typeof tasksRoot;
|
|
55
|
+
scopeDir: typeof scopeDir;
|
|
56
|
+
taskPath: typeof taskPath;
|
|
57
|
+
isClaimAbandoned: typeof isClaimAbandoned;
|
|
58
|
+
isUnblocked: typeof isUnblocked;
|
|
59
|
+
CLAIM_STALE_MS: number;
|
|
60
|
+
MIN_CLAIM_AGE_FOR_DEATH_CHECK_MS: number;
|
|
61
|
+
};
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=sharedTasks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sharedTasks.d.ts","sourceRoot":"","sources":["../../src/agent/sharedTasks.ts"],"names":[],"mappings":"AA6DA,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7E,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,4HAA4H;IAC5H,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8HAA8H;IAC9H,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kGAAkG;IAClG,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD,iBAAS,SAAS,IAAI,MAAM,CAE3B;AAED,iBAAS,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGzC;AAED,iBAAS,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEzD;AA4BD,0HAA0H;AAC1H,iBAAS,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAUnD;AAED,4GAA4G;AAC5G,iBAAS,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAG/D;AAED,0IAA0I;AAC1I,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAW7D;AAED,sJAAsJ;AACtJ,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GAC/C,UAAU,CAUZ;AAED,MAAM,MAAM,WAAW,GACnB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,WAAW,GAAG,iBAAiB,GAAG,SAAS,GAAG,aAAa,CAAA;CAAE,CAAC;AAEvF;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BxG;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACrF,OAAO,CAAC,WAAW,CAAC,CAoBtB;AAED,eAAO,MAAM,oBAAoB;;;;;;;;CAAqH,CAAC"}
|