flavor-code 1.4.1 → 1.4.3-beta.2
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/README.md +32 -2
- package/README.zh-CN.md +32 -2
- package/dist/{app-4YRKE7VI.js → app-G35YBHPR.js} +873 -641
- package/dist/apply-WOW3QU5I.js +51 -0
- package/dist/broker-cli-X5H46FZV.js +15 -0
- package/dist/check-CSKTV4O2.js +16 -0
- package/dist/chunk-3563SW4X.js +10513 -0
- package/dist/chunk-3UX2CGDW.js +1955 -0
- package/dist/chunk-4TMLXZVZ.js +75 -0
- package/dist/chunk-6F66UXBI.js +48 -0
- package/dist/{chunk-FAORPP4F.js → chunk-6URQ7FIF.js} +24 -88
- package/dist/chunk-6X62CGMA.js +193 -0
- package/dist/chunk-7Y64WNBB.js +8930 -0
- package/dist/chunk-AA4QNKTF.js +501 -0
- package/dist/chunk-BDX777JR.js +11 -0
- package/dist/chunk-CVP5OUHD.js +103 -0
- package/dist/chunk-EWD7FWTH.js +14547 -0
- package/dist/chunk-FAJ6YDZK.js +630 -0
- package/dist/{chunk-N5MXHJKD.js → chunk-GTFP2R5I.js} +1 -1
- package/dist/{chunk-7YFHRORG.js → chunk-HGTCIHWA.js} +383 -172
- package/dist/chunk-IDJAHEG6.js +43501 -0
- package/dist/chunk-IQEEFDEE.js +389 -0
- package/dist/chunk-IU3UBETC.js +30 -0
- package/dist/chunk-K7UGGWJV.js +1003 -0
- package/dist/chunk-KRWWCVXA.js +74 -0
- package/dist/chunk-OKHV7CTB.js +482 -0
- package/dist/chunk-PULWAH6E.js +474 -0
- package/dist/chunk-PZ5NKRVA.js +85 -0
- package/dist/chunk-RBAZLFKB.js +1022 -0
- package/dist/chunk-RV3EXUXM.js +30 -0
- package/dist/chunk-SJUIIHIA.js +2130 -0
- package/dist/{chunk-VMQWXQEC.js → chunk-T6RE76D2.js} +2003 -493
- package/dist/chunk-TIEOVQW6.js +61 -0
- package/dist/chunk-WLZUAVVU.js +15 -0
- package/dist/chunk-X442CJJH.js +114 -0
- package/dist/{chunk-RAYH6K2H.js → chunk-Y7EH4NVO.js} +1 -1
- package/dist/{chunk-S32JRDZK.js → chunk-ZKKVSR37.js} +6 -1
- package/dist/{claude-ink-JZXNSAQS.js → claude-ink-BE56DB6N.js} +9 -4
- package/dist/cli-3MF6EL2H.js +163 -0
- package/dist/cli-main.js +3805 -473
- package/dist/cli.js +50 -4
- package/dist/config/load.d.ts +8 -0
- package/dist/config/schema.d.ts +11 -0
- package/dist/context/manager.d.ts +2 -0
- package/dist/desktop/main.js +4303 -1171
- package/dist/desktop/preload.cjs +30 -0
- package/dist/desktop-renderer/assets/index-BWklSoOO.css +1 -0
- package/dist/desktop-renderer/assets/index-DCqIrF1S.js +153 -0
- package/dist/desktop-renderer/assets/{interactive-terminal-1agnYY5L.js → interactive-terminal-B6GbpzIn.js} +1 -1
- package/dist/desktop-renderer/index.html +2 -2
- package/dist/doctor-OHCGK52S.js +20 -0
- package/dist/load-QK6CMLDF.js +25 -0
- package/dist/log-ATFIPRNU.js +19 -0
- package/dist/manager-5SI4MPES.js +191 -0
- package/dist/models/anthropic.d.ts +3 -0
- package/dist/node-LOIXT44J.js +879 -0
- package/dist/plugin-worker-entry.js +1 -1
- package/dist/production-6DIZ6YLQ.js +46 -0
- package/dist/production.d.ts +28 -1
- package/dist/project-3QXE22TG.js +11 -0
- package/dist/react-M6JGXSJZ.js +7 -0
- package/dist/recorder-FAGJJH3N.js +11 -0
- package/dist/sdk/index.js +30 -8
- package/dist/server-2KLKPHPL.js +11 -0
- package/dist/session/store.d.ts +1 -0
- package/dist/store-ZIHN6363.js +18 -0
- package/dist/ui/commands.d.ts +1 -5
- package/dist/usage-summary-G5FBOJLK.js +13 -0
- package/dist/utils/execFileNoThrow.d.ts +1 -0
- package/dist/write-stream-CGD57URC.js +77 -0
- package/package.json +1 -1
- package/dist/chunk-O2PC44GY.js +0 -26120
- package/dist/chunk-ZJCAQXA7.js +0 -169
- package/dist/desktop-renderer/assets/index-BM9ugkh9.js +0 -154
- package/dist/desktop-renderer/assets/index-Bk6FYhyh.css +0 -1
- package/dist/load-UPOG3XQR.js +0 -19
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as createFlavorRequire } from "node:module"; const require = createFlavorRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
BrokerRequestSchema,
|
|
5
|
+
BrokerResponseSchema,
|
|
6
|
+
CoWorkPlanSchema,
|
|
7
|
+
CoWorkProposalParticipantsSchema,
|
|
8
|
+
CoWorkSnapshotSchema,
|
|
9
|
+
DeliveryReceiptSchema,
|
|
10
|
+
MAX_ACTIVE_PALS,
|
|
11
|
+
MAX_CONTROL_FRAME_BYTES,
|
|
12
|
+
MIN_UUID_PREFIX_LENGTH,
|
|
13
|
+
PalPresenceSchema,
|
|
14
|
+
encodeControlFrame,
|
|
15
|
+
normalizePalIdentity
|
|
16
|
+
} from "./chunk-IQEEFDEE.js";
|
|
17
|
+
|
|
18
|
+
// src/pals/broker-cli.ts
|
|
19
|
+
import { createHash as createHash2 } from "crypto";
|
|
20
|
+
import { Socket as Socket2 } from "net";
|
|
21
|
+
import { tmpdir } from "os";
|
|
22
|
+
import { join as join2 } from "path";
|
|
23
|
+
import { spawn } from "child_process";
|
|
24
|
+
|
|
25
|
+
// src/pals/broker.ts
|
|
26
|
+
import { createHash, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
|
|
27
|
+
import { chmod as chmod2, lstat, mkdir as mkdir3, unlink as unlink3 } from "fs/promises";
|
|
28
|
+
import { createServer, Socket } from "net";
|
|
29
|
+
import { dirname as dirname2 } from "path";
|
|
30
|
+
|
|
31
|
+
// src/pals/auth.ts
|
|
32
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
33
|
+
import { chmod, link, mkdir, open, readFile, unlink } from "fs/promises";
|
|
34
|
+
import { homedir } from "os";
|
|
35
|
+
import { join } from "path";
|
|
36
|
+
var PAL_AUTH_TOKEN_BYTES = 32;
|
|
37
|
+
var TOKEN_PATTERN = /^[a-f0-9]{64}$/;
|
|
38
|
+
async function loadOrCreatePalAuthToken(options = {}) {
|
|
39
|
+
const directory = join(options.home ?? homedir(), ".flavor-code", "pals");
|
|
40
|
+
const tokenPath = join(directory, "auth-token");
|
|
41
|
+
await mkdir(directory, { recursive: true, mode: 448 });
|
|
42
|
+
await chmod(directory, 448);
|
|
43
|
+
const token = (options.random ?? randomBytes)(PAL_AUTH_TOKEN_BYTES).toString("hex");
|
|
44
|
+
const temporaryPath = join(directory, `.auth-token-${process.pid}-${randomUUID()}.tmp`);
|
|
45
|
+
let handle;
|
|
46
|
+
try {
|
|
47
|
+
handle = await open(temporaryPath, "wx", 384);
|
|
48
|
+
try {
|
|
49
|
+
await handle.writeFile(`${token}
|
|
50
|
+
`, "utf8");
|
|
51
|
+
await handle.sync();
|
|
52
|
+
} finally {
|
|
53
|
+
await handle.close();
|
|
54
|
+
handle = void 0;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await link(temporaryPath, tokenPath);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error.code !== "EEXIST") throw error;
|
|
60
|
+
}
|
|
61
|
+
} finally {
|
|
62
|
+
await handle?.close();
|
|
63
|
+
await unlink(temporaryPath).catch((error) => {
|
|
64
|
+
if (error.code !== "ENOENT") throw error;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const stored = (await readFile(tokenPath, "utf8")).trim();
|
|
68
|
+
if (!TOKEN_PATTERN.test(stored)) throw new Error("Pals authentication token store is invalid");
|
|
69
|
+
await chmod(tokenPath, 384);
|
|
70
|
+
return stored;
|
|
71
|
+
}
|
|
72
|
+
function validatePalAuthToken(token) {
|
|
73
|
+
if (!TOKEN_PATTERN.test(token)) throw new Error("Pals authentication token must be 32 bytes encoded as lowercase hex");
|
|
74
|
+
return token;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/pals/lifecycle.ts
|
|
78
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
79
|
+
import { mkdir as mkdir2, open as open2, readFile as readFile2, rename, stat, unlink as unlink2 } from "fs/promises";
|
|
80
|
+
import { dirname } from "path";
|
|
81
|
+
function defaultProcessAlive(pid) {
|
|
82
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return true;
|
|
83
|
+
try {
|
|
84
|
+
process.kill(pid, 0);
|
|
85
|
+
return true;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return error.code !== "ESRCH";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function parseMetadata(value) {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(value);
|
|
93
|
+
if (!Number.isSafeInteger(parsed.pid) || parsed.pid <= 0 || !Number.isFinite(parsed.createdAt) || typeof parsed.nonce !== "string" || parsed.nonce.length < 1 || parsed.nonce.length > 128) return void 0;
|
|
94
|
+
return { pid: parsed.pid, createdAt: parsed.createdAt, nonce: parsed.nonce };
|
|
95
|
+
} catch {
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function acquirePalFileLock(options) {
|
|
100
|
+
if (await options.endpointLive()) return void 0;
|
|
101
|
+
await mkdir2(dirname(options.path), { recursive: true, mode: 448 });
|
|
102
|
+
const pid = options.pid ?? process.pid;
|
|
103
|
+
const now = options.now ?? Date.now;
|
|
104
|
+
const nonce = randomUUID2();
|
|
105
|
+
const metadata = { pid, createdAt: now(), nonce };
|
|
106
|
+
let handle;
|
|
107
|
+
try {
|
|
108
|
+
handle = await open2(options.path, "wx", 384);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error.code !== "EEXIST") throw error;
|
|
111
|
+
const [contents, info] = await Promise.all([readFile2(options.path, "utf8").catch(() => ""), stat(options.path).catch(() => void 0)]);
|
|
112
|
+
if (await options.endpointLive()) return void 0;
|
|
113
|
+
const existing = parseMetadata(contents);
|
|
114
|
+
if (existing !== void 0 && (options.processAlive ?? defaultProcessAlive)(existing.pid)) return void 0;
|
|
115
|
+
if (existing === void 0 && (info === void 0 || now() - info.mtimeMs <= (options.corruptGraceMs ?? 5e3))) return void 0;
|
|
116
|
+
const tombstone = `${options.path}.reclaim-${pid}-${nonce}`;
|
|
117
|
+
try {
|
|
118
|
+
await rename(options.path, tombstone);
|
|
119
|
+
} catch (renameError) {
|
|
120
|
+
if (["ENOENT", "EEXIST"].includes(renameError.code ?? "")) return void 0;
|
|
121
|
+
throw renameError;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
handle = await open2(options.path, "wx", 384);
|
|
125
|
+
} catch (openError) {
|
|
126
|
+
if (openError.code !== "EEXIST") throw openError;
|
|
127
|
+
return void 0;
|
|
128
|
+
} finally {
|
|
129
|
+
await unlink2(tombstone).catch((unlinkError) => {
|
|
130
|
+
if (unlinkError.code !== "ENOENT") throw unlinkError;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
await handle.writeFile(JSON.stringify(metadata), "utf8");
|
|
136
|
+
await handle.sync();
|
|
137
|
+
} finally {
|
|
138
|
+
await handle.close();
|
|
139
|
+
}
|
|
140
|
+
const release = async () => {
|
|
141
|
+
const current = parseMetadata(await readFile2(options.path, "utf8").catch(() => ""));
|
|
142
|
+
if (current?.nonce !== nonce) return;
|
|
143
|
+
await unlink2(options.path).catch((error) => {
|
|
144
|
+
if (error.code !== "ENOENT") throw error;
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
if (await options.endpointLive()) {
|
|
148
|
+
await release();
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
return { path: options.path, release };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/pals/broker.ts
|
|
155
|
+
var BrokerStateError = class extends Error {
|
|
156
|
+
constructor(code, message) {
|
|
157
|
+
super(message);
|
|
158
|
+
this.code = code;
|
|
159
|
+
this.name = "BrokerStateError";
|
|
160
|
+
}
|
|
161
|
+
code;
|
|
162
|
+
};
|
|
163
|
+
var WINDOWS_PIPE_PREFIX = "\\\\.\\pipe\\";
|
|
164
|
+
function canConnect(address) {
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
const socket = new Socket();
|
|
167
|
+
const finish = (connected) => {
|
|
168
|
+
socket.removeAllListeners();
|
|
169
|
+
socket.destroy();
|
|
170
|
+
resolve(connected);
|
|
171
|
+
};
|
|
172
|
+
socket.once("connect", () => finish(true));
|
|
173
|
+
socket.once("error", () => finish(false));
|
|
174
|
+
socket.connect(address);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function isWindowsPipeAddress(address, platform = process.platform) {
|
|
178
|
+
return platform === "win32" && address.toLowerCase().startsWith(WINDOWS_PIPE_PREFIX);
|
|
179
|
+
}
|
|
180
|
+
var PalBrokerServer = class {
|
|
181
|
+
address;
|
|
182
|
+
lastActivityAt = Date.now();
|
|
183
|
+
platform;
|
|
184
|
+
state;
|
|
185
|
+
maxFrameBytes;
|
|
186
|
+
server;
|
|
187
|
+
connections = /* @__PURE__ */ new Set();
|
|
188
|
+
palConnections = /* @__PURE__ */ new Map();
|
|
189
|
+
started = false;
|
|
190
|
+
ownsAddress = false;
|
|
191
|
+
closing;
|
|
192
|
+
authToken;
|
|
193
|
+
authHome;
|
|
194
|
+
processAlive;
|
|
195
|
+
probeAddress;
|
|
196
|
+
endpointLock;
|
|
197
|
+
constructor(options) {
|
|
198
|
+
this.address = options.address;
|
|
199
|
+
this.platform = options.platform ?? process.platform;
|
|
200
|
+
this.state = options.state ?? new PalBrokerState();
|
|
201
|
+
this.maxFrameBytes = options.maxFrameBytes ?? MAX_CONTROL_FRAME_BYTES;
|
|
202
|
+
if (!Number.isSafeInteger(this.maxFrameBytes) || this.maxFrameBytes <= 0 || this.maxFrameBytes > MAX_CONTROL_FRAME_BYTES) {
|
|
203
|
+
throw new Error(`maxFrameBytes must be a positive integer not exceeding ${MAX_CONTROL_FRAME_BYTES}`);
|
|
204
|
+
}
|
|
205
|
+
this.authToken = options.authToken === void 0 ? void 0 : validatePalAuthToken(options.authToken);
|
|
206
|
+
this.authHome = options.authHome;
|
|
207
|
+
this.processAlive = options.processAlive;
|
|
208
|
+
this.probeAddress = options.probeAddress ?? canConnect;
|
|
209
|
+
this.server = createServer((socket) => this.accept(socket));
|
|
210
|
+
}
|
|
211
|
+
get connectionCount() {
|
|
212
|
+
return this.connections.size;
|
|
213
|
+
}
|
|
214
|
+
async start() {
|
|
215
|
+
if (this.started) return;
|
|
216
|
+
this.authToken ??= await loadOrCreatePalAuthToken(this.authHome === void 0 ? {} : { home: this.authHome });
|
|
217
|
+
try {
|
|
218
|
+
await this.prepareAddress();
|
|
219
|
+
await new Promise((resolve, reject) => {
|
|
220
|
+
const onError = (error) => {
|
|
221
|
+
this.server.off("listening", onListening);
|
|
222
|
+
reject(error);
|
|
223
|
+
};
|
|
224
|
+
const onListening = () => {
|
|
225
|
+
this.server.off("error", onError);
|
|
226
|
+
this.started = true;
|
|
227
|
+
this.ownsAddress = true;
|
|
228
|
+
resolve();
|
|
229
|
+
};
|
|
230
|
+
this.server.once("error", onError);
|
|
231
|
+
this.server.once("listening", onListening);
|
|
232
|
+
this.server.listen(this.address);
|
|
233
|
+
});
|
|
234
|
+
} catch (error) {
|
|
235
|
+
await this.releaseEndpointLock();
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
sweep() {
|
|
240
|
+
const active = new Set(this.state.list().map(({ id }) => id));
|
|
241
|
+
for (const [palId, connection] of this.palConnections) {
|
|
242
|
+
if (active.has(palId)) continue;
|
|
243
|
+
this.palConnections.delete(palId);
|
|
244
|
+
connection.socket.destroy();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
close() {
|
|
248
|
+
if (this.closing !== void 0) return this.closing;
|
|
249
|
+
this.closing = this.closeOnce();
|
|
250
|
+
return this.closing;
|
|
251
|
+
}
|
|
252
|
+
async closeOnce() {
|
|
253
|
+
const ownsAddress = this.ownsAddress;
|
|
254
|
+
for (const connection of this.connections) connection.socket.destroy();
|
|
255
|
+
if (this.started) {
|
|
256
|
+
await new Promise((resolve) => this.server.close(() => resolve()));
|
|
257
|
+
this.started = false;
|
|
258
|
+
}
|
|
259
|
+
this.ownsAddress = false;
|
|
260
|
+
if (ownsAddress && !isWindowsPipeAddress(this.address, this.platform)) await this.unlinkKnownSocket();
|
|
261
|
+
await this.releaseEndpointLock();
|
|
262
|
+
}
|
|
263
|
+
async prepareAddress() {
|
|
264
|
+
if (isWindowsPipeAddress(this.address, this.platform)) return;
|
|
265
|
+
const parent = dirname2(this.address);
|
|
266
|
+
await mkdir3(parent, { recursive: true, mode: 448 });
|
|
267
|
+
await chmod2(parent, 448);
|
|
268
|
+
this.endpointLock = await acquirePalFileLock({
|
|
269
|
+
path: `${this.address}.owner.lock`,
|
|
270
|
+
endpointLive: () => this.probeAddress(this.address),
|
|
271
|
+
...this.processAlive === void 0 ? {} : { processAlive: this.processAlive }
|
|
272
|
+
});
|
|
273
|
+
if (this.endpointLock === void 0) throw new Error(`Pals endpoint '${this.address}' is active or owned`);
|
|
274
|
+
try {
|
|
275
|
+
const stat2 = await lstat(this.address);
|
|
276
|
+
if (!stat2.isSocket()) throw new Error(`Refusing to unlink '${this.address}' because it is not a socket`);
|
|
277
|
+
await unlink3(this.address);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (error.code !== "ENOENT") throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async releaseEndpointLock() {
|
|
283
|
+
const lock = this.endpointLock;
|
|
284
|
+
this.endpointLock = void 0;
|
|
285
|
+
await lock?.release();
|
|
286
|
+
}
|
|
287
|
+
async unlinkKnownSocket() {
|
|
288
|
+
try {
|
|
289
|
+
const stat2 = await lstat(this.address);
|
|
290
|
+
if (stat2.isSocket()) await unlink3(this.address);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error.code !== "ENOENT") throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
accept(socket) {
|
|
296
|
+
const connection = { socket, buffer: Buffer.alloc(0) };
|
|
297
|
+
this.connections.add(connection);
|
|
298
|
+
this.lastActivityAt = Date.now();
|
|
299
|
+
socket.on("data", (chunk) => this.receive(connection, chunk));
|
|
300
|
+
socket.on("error", () => void 0);
|
|
301
|
+
socket.once("close", () => this.remove(connection));
|
|
302
|
+
}
|
|
303
|
+
remove(connection) {
|
|
304
|
+
this.connections.delete(connection);
|
|
305
|
+
if (connection.palId !== void 0 && this.palConnections.get(connection.palId) === connection) {
|
|
306
|
+
this.palConnections.delete(connection.palId);
|
|
307
|
+
this.state.disconnect(connection.palId);
|
|
308
|
+
}
|
|
309
|
+
this.lastActivityAt = Date.now();
|
|
310
|
+
}
|
|
311
|
+
receive(connection, chunk) {
|
|
312
|
+
this.lastActivityAt = Date.now();
|
|
313
|
+
connection.buffer = Buffer.concat([connection.buffer, chunk]);
|
|
314
|
+
while (!connection.socket.destroyed) {
|
|
315
|
+
const newline = connection.buffer.indexOf(10);
|
|
316
|
+
if (newline < 0) {
|
|
317
|
+
if (connection.buffer.byteLength > this.maxFrameBytes) connection.socket.destroy();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (newline > this.maxFrameBytes) {
|
|
321
|
+
connection.socket.destroy();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const frame = connection.buffer.subarray(0, newline).toString("utf8");
|
|
325
|
+
connection.buffer = connection.buffer.subarray(newline + 1);
|
|
326
|
+
this.handleFrame(connection, frame);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
handleFrame(connection, frame) {
|
|
330
|
+
let raw;
|
|
331
|
+
try {
|
|
332
|
+
raw = JSON.parse(frame);
|
|
333
|
+
} catch {
|
|
334
|
+
connection.socket.destroy();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const parsed = BrokerRequestSchema.safeParse(raw);
|
|
338
|
+
if (!parsed.success) {
|
|
339
|
+
const requestId = typeof raw === "object" && raw !== null && "requestId" in raw ? raw.requestId : void 0;
|
|
340
|
+
if (typeof requestId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(requestId)) {
|
|
341
|
+
connection.socket.destroy();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.send(connection, { version: 1, type: "error", requestId, code: "invalid-request", message: "Invalid request" });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
void this.dispatch(connection, parsed.data);
|
|
348
|
+
}
|
|
349
|
+
dispatch(connection, request) {
|
|
350
|
+
try {
|
|
351
|
+
if (request.type === "register") {
|
|
352
|
+
if (!this.authenticates(request.authToken)) {
|
|
353
|
+
this.send(connection, { version: 1, type: "error", requestId: request.requestId, code: "authentication-failed", message: "Local authentication failed" });
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const id = request.presence.id.toLowerCase();
|
|
357
|
+
if (connection.palId !== void 0 && connection.palId !== id) {
|
|
358
|
+
throw new BrokerStateError("invalid-transition", "A connection cannot change its pal identity");
|
|
359
|
+
}
|
|
360
|
+
const occupied = this.palConnections.get(id);
|
|
361
|
+
if (occupied !== void 0 && occupied !== connection) {
|
|
362
|
+
throw new BrokerStateError("alias-conflict", `Pal '${id}' is already connected`);
|
|
363
|
+
}
|
|
364
|
+
const presence = this.state.register({ id, alias: request.presence.alias, projectPath: request.presence.projectPath });
|
|
365
|
+
connection.palId = id;
|
|
366
|
+
this.palConnections.set(id, connection);
|
|
367
|
+
this.ok(connection, request.requestId, presence);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const actorId = this.requireRegistered(connection);
|
|
371
|
+
switch (request.type) {
|
|
372
|
+
case "heartbeat":
|
|
373
|
+
this.ok(connection, request.requestId, this.state.heartbeat(actorId));
|
|
374
|
+
return;
|
|
375
|
+
case "list":
|
|
376
|
+
this.ok(connection, request.requestId, this.state.list());
|
|
377
|
+
return;
|
|
378
|
+
case "disconnect":
|
|
379
|
+
this.state.disconnect(actorId);
|
|
380
|
+
if (this.palConnections.get(actorId) === connection) this.palConnections.delete(actorId);
|
|
381
|
+
this.ok(connection, request.requestId, null);
|
|
382
|
+
connection.socket.end();
|
|
383
|
+
return;
|
|
384
|
+
case "chat": {
|
|
385
|
+
const result = this.state.routeChat(actorId, request);
|
|
386
|
+
this.deliver(result.deliveries);
|
|
387
|
+
this.ok(connection, request.requestId, result.receipt);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
case "task": {
|
|
391
|
+
const result = this.state.routeTask(actorId, request);
|
|
392
|
+
this.deliver(result.deliveries);
|
|
393
|
+
this.ok(connection, request.requestId, result.receipt);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
case "cowork-propose": {
|
|
397
|
+
const result = this.state.proposeCoWork(actorId, request);
|
|
398
|
+
this.deliverCoWork(result.events, result.snapshot.participants.map(({ palId }) => palId));
|
|
399
|
+
this.ok(connection, request.requestId, result.snapshot);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
case "cowork-accept": {
|
|
403
|
+
const result = this.state.acceptCoWork(actorId, request.coWorkId, request.epoch);
|
|
404
|
+
this.deliverCoWork(result.events, result.snapshot.participants.map(({ palId }) => palId));
|
|
405
|
+
this.ok(connection, request.requestId, result.snapshot);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
case "cowork-plan": {
|
|
409
|
+
const result = this.state.submitCoWorkPlan(actorId, request.plan);
|
|
410
|
+
this.deliverCoWork(result.events, result.snapshot.participants.map(({ palId }) => palId));
|
|
411
|
+
this.ok(connection, request.requestId, result.snapshot);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
case "cowork-plan-accept": {
|
|
415
|
+
const result = this.state.acceptCoWorkPlan(actorId, request);
|
|
416
|
+
this.deliverCoWork(result.events, result.snapshot.participants.map(({ palId }) => palId));
|
|
417
|
+
this.ok(connection, request.requestId, result.snapshot);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
case "cowork-ready": {
|
|
421
|
+
const result = this.state.markCoWorkReady(actorId, request);
|
|
422
|
+
this.deliverCoWork(result.events, result.snapshot.participants.map(({ palId }) => palId));
|
|
423
|
+
this.ok(connection, request.requestId, result.snapshot);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
case "cowork-complete": {
|
|
427
|
+
const result = this.state.completeCoWork(actorId, request.detail === void 0 ? {
|
|
428
|
+
coWorkId: request.coWorkId,
|
|
429
|
+
epoch: request.epoch,
|
|
430
|
+
planHash: request.planHash,
|
|
431
|
+
passed: request.passed
|
|
432
|
+
} : {
|
|
433
|
+
coWorkId: request.coWorkId,
|
|
434
|
+
epoch: request.epoch,
|
|
435
|
+
planHash: request.planHash,
|
|
436
|
+
passed: request.passed,
|
|
437
|
+
detail: request.detail
|
|
438
|
+
});
|
|
439
|
+
this.deliverCoWork(result.events, result.snapshot.participants.map(({ palId }) => palId));
|
|
440
|
+
this.ok(connection, request.requestId, result.snapshot);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
case "cowork-integration": {
|
|
444
|
+
const result = this.state.integrateCoWork(actorId, {
|
|
445
|
+
coWorkId: request.coWorkId,
|
|
446
|
+
epoch: request.epoch,
|
|
447
|
+
planHash: request.planHash,
|
|
448
|
+
passed: request.passed,
|
|
449
|
+
evidence: request.evidence
|
|
450
|
+
});
|
|
451
|
+
this.deliverCoWork(result.events, result.snapshot.participants.map(({ palId }) => palId));
|
|
452
|
+
this.ok(connection, request.requestId, result.snapshot);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
case "cowork-get":
|
|
456
|
+
this.ok(connection, request.requestId, this.state.getCoWork(request.coWorkId));
|
|
457
|
+
return;
|
|
458
|
+
case "cowork-cancel": {
|
|
459
|
+
const result = this.state.cancelCoWork(actorId, request.coWorkId, request.reason);
|
|
460
|
+
this.deliverCoWork(result.events, result.snapshot.participants.map(({ palId }) => palId));
|
|
461
|
+
this.ok(connection, request.requestId, result.snapshot);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} catch (error) {
|
|
466
|
+
const stateError = error instanceof BrokerStateError ? error : void 0;
|
|
467
|
+
const code = stateError?.code === "capacity" ? "invalid-transition" : stateError?.code ?? "invalid-request";
|
|
468
|
+
this.send(connection, {
|
|
469
|
+
version: 1,
|
|
470
|
+
type: "error",
|
|
471
|
+
requestId: request.requestId,
|
|
472
|
+
code,
|
|
473
|
+
message: error instanceof Error ? error.message : "Request failed"
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
requireRegistered(connection) {
|
|
478
|
+
if (connection.palId === void 0) throw new BrokerStateError("invalid-transition", "Register before sending requests");
|
|
479
|
+
return connection.palId;
|
|
480
|
+
}
|
|
481
|
+
authenticates(candidate) {
|
|
482
|
+
if (candidate === void 0 || this.authToken === void 0) return false;
|
|
483
|
+
const expected = Buffer.from(this.authToken, "hex");
|
|
484
|
+
const received = Buffer.from(candidate, "hex");
|
|
485
|
+
return received.byteLength === expected.byteLength && timingSafeEqual(received, expected);
|
|
486
|
+
}
|
|
487
|
+
ok(connection, requestId, data) {
|
|
488
|
+
this.send(connection, { version: 1, type: "ok", requestId, data });
|
|
489
|
+
}
|
|
490
|
+
send(connection, response) {
|
|
491
|
+
if (connection.socket.destroyed || !connection.socket.writable) return;
|
|
492
|
+
try {
|
|
493
|
+
connection.socket.write(encodeControlFrame(BrokerResponseSchema.parse(response)));
|
|
494
|
+
} catch {
|
|
495
|
+
connection.socket.destroy();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
deliver(deliveries) {
|
|
499
|
+
for (const delivery of deliveries) {
|
|
500
|
+
const recipient = this.palConnections.get(delivery.recipientId);
|
|
501
|
+
if (recipient !== void 0) this.send(recipient, { version: 1, type: "event", event: delivery.event });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
deliverCoWork(events, participantIds) {
|
|
505
|
+
for (const event of events) {
|
|
506
|
+
this.deliver(participantIds.map((recipientId) => ({ recipientId, event })));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
var DEFAULT_HEARTBEAT_TIMEOUT_MS = 3e4;
|
|
511
|
+
var DEFAULT_DEDUP_LIMIT = 2048;
|
|
512
|
+
var DEFAULT_MAX_PALS = MAX_ACTIVE_PALS;
|
|
513
|
+
var DEFAULT_MAX_COWORKS = 64;
|
|
514
|
+
function positiveInteger(value, name) {
|
|
515
|
+
if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`);
|
|
516
|
+
return value;
|
|
517
|
+
}
|
|
518
|
+
function clone(value) {
|
|
519
|
+
return structuredClone(value);
|
|
520
|
+
}
|
|
521
|
+
function canonicalize(value) {
|
|
522
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
523
|
+
if (value !== null && typeof value === "object") {
|
|
524
|
+
return Object.fromEntries(
|
|
525
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, canonicalize(child)])
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
return value;
|
|
529
|
+
}
|
|
530
|
+
function canonicalPlanHash(input) {
|
|
531
|
+
const plan = CoWorkPlanSchema.parse(input);
|
|
532
|
+
return createHash("sha256").update(JSON.stringify(canonicalize(plan)), "utf8").digest("hex");
|
|
533
|
+
}
|
|
534
|
+
function sameParticipants(left, right) {
|
|
535
|
+
const normalize = (participants) => participants.map((participant) => `${participant.palId.toLowerCase()}:${participant.required ? "1" : "0"}`).sort();
|
|
536
|
+
return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
|
|
537
|
+
}
|
|
538
|
+
function includesAll(values, required) {
|
|
539
|
+
const present = new Set(values);
|
|
540
|
+
return required.every((id) => present.has(id));
|
|
541
|
+
}
|
|
542
|
+
function appendUnique(values, value) {
|
|
543
|
+
return values.includes(value) ? [...values] : [...values, value];
|
|
544
|
+
}
|
|
545
|
+
var PalBrokerState = class {
|
|
546
|
+
now;
|
|
547
|
+
idFactory;
|
|
548
|
+
heartbeatTimeoutMs;
|
|
549
|
+
dedupLimit;
|
|
550
|
+
maxPals;
|
|
551
|
+
maxCoWorks;
|
|
552
|
+
pals = /* @__PURE__ */ new Map();
|
|
553
|
+
aliases = /* @__PURE__ */ new Map();
|
|
554
|
+
delivered = /* @__PURE__ */ new Map();
|
|
555
|
+
coWorks = /* @__PURE__ */ new Map();
|
|
556
|
+
constructor(options = {}) {
|
|
557
|
+
this.now = options.now ?? Date.now;
|
|
558
|
+
this.idFactory = options.idFactory ?? randomUUID3;
|
|
559
|
+
this.heartbeatTimeoutMs = positiveInteger(options.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS, "heartbeatTimeoutMs");
|
|
560
|
+
this.dedupLimit = positiveInteger(options.dedupLimit ?? DEFAULT_DEDUP_LIMIT, "dedupLimit");
|
|
561
|
+
this.maxPals = positiveInteger(options.maxPals ?? DEFAULT_MAX_PALS, "maxPals");
|
|
562
|
+
if (this.maxPals > MAX_ACTIVE_PALS) throw new Error(`maxPals must not exceed ${MAX_ACTIVE_PALS}`);
|
|
563
|
+
this.maxCoWorks = positiveInteger(options.maxCoWorks ?? DEFAULT_MAX_COWORKS, "maxCoWorks");
|
|
564
|
+
}
|
|
565
|
+
register(registration) {
|
|
566
|
+
this.sweepExpired();
|
|
567
|
+
const id = registration.id.toLowerCase();
|
|
568
|
+
const aliasKey = normalizePalIdentity(registration.alias);
|
|
569
|
+
const claimedBy = this.aliases.get(aliasKey);
|
|
570
|
+
if (claimedBy !== void 0 && claimedBy !== id) {
|
|
571
|
+
throw new BrokerStateError("alias-conflict", `Alias '${registration.alias}' is already active`);
|
|
572
|
+
}
|
|
573
|
+
if (!this.pals.has(id) && this.pals.size >= this.maxPals) {
|
|
574
|
+
throw new BrokerStateError("capacity", "Pal capacity reached");
|
|
575
|
+
}
|
|
576
|
+
const existing = this.pals.get(id);
|
|
577
|
+
if (existing !== void 0) this.aliases.delete(existing.presence.alias.toLocaleLowerCase("en-US"));
|
|
578
|
+
const now = this.now();
|
|
579
|
+
const presence = PalPresenceSchema.parse({
|
|
580
|
+
version: 1,
|
|
581
|
+
id,
|
|
582
|
+
alias: registration.alias,
|
|
583
|
+
projectPath: registration.projectPath,
|
|
584
|
+
connectedAt: existing?.presence.connectedAt ?? new Date(now).toISOString(),
|
|
585
|
+
lastSeenAt: new Date(now).toISOString()
|
|
586
|
+
});
|
|
587
|
+
this.pals.set(id, { presence, lastSeenMs: now });
|
|
588
|
+
this.aliases.set(aliasKey, id);
|
|
589
|
+
return clone(presence);
|
|
590
|
+
}
|
|
591
|
+
list() {
|
|
592
|
+
this.sweepExpired();
|
|
593
|
+
return [...this.pals.values()].map(({ presence }) => clone(presence)).sort((left, right) => left.id.localeCompare(right.id));
|
|
594
|
+
}
|
|
595
|
+
resolveTarget(target) {
|
|
596
|
+
this.sweepExpired();
|
|
597
|
+
const normalized = normalizePalIdentity(target);
|
|
598
|
+
const aliasId = this.aliases.get(normalized);
|
|
599
|
+
if (aliasId !== void 0) return clone(this.pals.get(aliasId).presence);
|
|
600
|
+
const exact = this.pals.get(normalized);
|
|
601
|
+
if (exact !== void 0) return clone(exact.presence);
|
|
602
|
+
if (normalized.length >= MIN_UUID_PREFIX_LENGTH) {
|
|
603
|
+
const matches = [...this.pals.entries()].filter(([id]) => id.startsWith(normalized));
|
|
604
|
+
if (matches.length === 1) return clone(matches[0][1].presence);
|
|
605
|
+
if (matches.length > 1) {
|
|
606
|
+
throw new BrokerStateError("ambiguous-target", `Target '${target}' matches multiple active pals`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
throw new BrokerStateError("not-found", `No active pal matches '${target}'`);
|
|
610
|
+
}
|
|
611
|
+
heartbeat(palId) {
|
|
612
|
+
this.sweepExpired();
|
|
613
|
+
const key = palId.toLowerCase();
|
|
614
|
+
const record = this.pals.get(key);
|
|
615
|
+
if (record === void 0) throw new BrokerStateError("not-found", `Pal '${palId}' is not active`);
|
|
616
|
+
const now = this.now();
|
|
617
|
+
record.lastSeenMs = now;
|
|
618
|
+
record.presence = { ...record.presence, lastSeenAt: new Date(now).toISOString() };
|
|
619
|
+
return clone(record.presence);
|
|
620
|
+
}
|
|
621
|
+
disconnect(palId) {
|
|
622
|
+
const key = palId.toLowerCase();
|
|
623
|
+
const record = this.pals.get(key);
|
|
624
|
+
if (record === void 0) return;
|
|
625
|
+
this.pals.delete(key);
|
|
626
|
+
this.aliases.delete(record.presence.alias.toLocaleLowerCase("en-US"));
|
|
627
|
+
}
|
|
628
|
+
routeChat(senderId, input) {
|
|
629
|
+
const sender = this.requirePal(senderId);
|
|
630
|
+
const recipient = this.resolveTarget(input.target);
|
|
631
|
+
const duplicate = this.duplicateReceipt(input.messageId);
|
|
632
|
+
if (duplicate !== void 0) return { receipt: duplicate, deliveries: [] };
|
|
633
|
+
const event = {
|
|
634
|
+
version: 1,
|
|
635
|
+
type: "chat-event",
|
|
636
|
+
messageId: input.messageId,
|
|
637
|
+
senderId: sender.id,
|
|
638
|
+
recipientId: recipient.id,
|
|
639
|
+
message: input.message
|
|
640
|
+
};
|
|
641
|
+
const receipt = this.rememberDelivery(input.messageId, [recipient.id]);
|
|
642
|
+
return { receipt, deliveries: [{ recipientId: recipient.id, event }] };
|
|
643
|
+
}
|
|
644
|
+
routeTask(senderId, input) {
|
|
645
|
+
const sender = this.requirePal(senderId);
|
|
646
|
+
const recipient = this.resolveTarget(input.target);
|
|
647
|
+
const duplicate = this.duplicateReceipt(input.messageId);
|
|
648
|
+
if (duplicate !== void 0) return { receipt: duplicate, deliveries: [] };
|
|
649
|
+
const event = {
|
|
650
|
+
version: 1,
|
|
651
|
+
type: "task-event",
|
|
652
|
+
messageId: input.messageId,
|
|
653
|
+
taskId: this.idFactory(),
|
|
654
|
+
senderId: sender.id,
|
|
655
|
+
recipientId: recipient.id,
|
|
656
|
+
status: "accepted",
|
|
657
|
+
detail: input.goal
|
|
658
|
+
};
|
|
659
|
+
const receipt = this.rememberDelivery(input.messageId, [recipient.id]);
|
|
660
|
+
return { receipt, deliveries: [{ recipientId: recipient.id, event }] };
|
|
661
|
+
}
|
|
662
|
+
proposeCoWork(actorId, input) {
|
|
663
|
+
const actor = this.requirePal(actorId);
|
|
664
|
+
if (this.coWorks.has(input.coWorkId)) throw new BrokerStateError("invalid-transition", "Co-work already exists");
|
|
665
|
+
const roster = CoWorkProposalParticipantsSchema.safeParse(input.participants);
|
|
666
|
+
if (!roster.success) {
|
|
667
|
+
throw new BrokerStateError("invalid-transition", "Co-work requires at least two distinct participants");
|
|
668
|
+
}
|
|
669
|
+
for (const participant of roster.data) this.requirePal(participant.palId);
|
|
670
|
+
if (!roster.data.some((participant) => participant.palId === actor.id)) {
|
|
671
|
+
throw new BrokerStateError("invalid-transition", "Proposer must be a participant");
|
|
672
|
+
}
|
|
673
|
+
const integrationOwner = roster.data.find(({ required }) => required);
|
|
674
|
+
if (integrationOwner === void 0) {
|
|
675
|
+
throw new BrokerStateError("invalid-transition", "Co-work requires at least one required participant");
|
|
676
|
+
}
|
|
677
|
+
const acceptedParticipantIds = [actor.id];
|
|
678
|
+
const requiredParticipantIds = roster.data.filter(({ required }) => required).map(({ palId }) => palId);
|
|
679
|
+
const snapshot = CoWorkSnapshotSchema.parse({
|
|
680
|
+
version: 1,
|
|
681
|
+
coWorkId: input.coWorkId,
|
|
682
|
+
epoch: 1,
|
|
683
|
+
phase: includesAll(acceptedParticipantIds, requiredParticipantIds) ? "planning" : "proposed",
|
|
684
|
+
goal: input.goal,
|
|
685
|
+
participants: roster.data,
|
|
686
|
+
integrationOwnerId: integrationOwner.palId,
|
|
687
|
+
acceptedParticipantIds,
|
|
688
|
+
planHash: null,
|
|
689
|
+
plan: null,
|
|
690
|
+
planAcceptedParticipantIds: [],
|
|
691
|
+
readyParticipantIds: [],
|
|
692
|
+
completedParticipantIds: [],
|
|
693
|
+
completionAssertions: [],
|
|
694
|
+
integration: null
|
|
695
|
+
});
|
|
696
|
+
const transition = this.transition(snapshot, actorId, "PROPOSE");
|
|
697
|
+
const evictionCandidate = this.coWorkEvictionCandidate();
|
|
698
|
+
const committed = new Map(this.coWorks);
|
|
699
|
+
if (evictionCandidate !== void 0) committed.delete(evictionCandidate);
|
|
700
|
+
committed.set(input.coWorkId, { snapshot });
|
|
701
|
+
this.coWorks = committed;
|
|
702
|
+
return transition;
|
|
703
|
+
}
|
|
704
|
+
acceptCoWork(actorId, coWorkId, epoch) {
|
|
705
|
+
const record = this.requireCoWork(coWorkId);
|
|
706
|
+
this.assertEpoch(record.snapshot, epoch);
|
|
707
|
+
this.requireRequiredParticipant(record.snapshot, actorId);
|
|
708
|
+
if (record.snapshot.phase !== "proposed") throw new BrokerStateError("invalid-transition", "Co-work is not awaiting acceptance");
|
|
709
|
+
return this.mutateCoWork(record, actorId, (snapshot) => {
|
|
710
|
+
snapshot.acceptedParticipantIds = appendUnique(snapshot.acceptedParticipantIds, actorId);
|
|
711
|
+
if (includesAll(snapshot.acceptedParticipantIds, this.requiredIds(snapshot))) snapshot.phase = "planning";
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
submitCoWorkPlan(actorId, input) {
|
|
715
|
+
const plan = CoWorkPlanSchema.parse(input);
|
|
716
|
+
const record = this.requireCoWork(plan.coWorkId);
|
|
717
|
+
this.requireRequiredParticipant(record.snapshot, actorId);
|
|
718
|
+
if (record.snapshot.phase !== "planning" && record.snapshot.phase !== "prepared") {
|
|
719
|
+
throw new BrokerStateError("invalid-transition", "Co-work is not accepting a plan");
|
|
720
|
+
}
|
|
721
|
+
const expectedEpoch = record.snapshot.plan === null ? record.snapshot.epoch : record.snapshot.epoch + 1;
|
|
722
|
+
if (plan.epoch !== expectedEpoch) throw new BrokerStateError("stale-epoch", `Expected plan epoch ${expectedEpoch}`);
|
|
723
|
+
if (plan.goal !== record.snapshot.goal || !sameParticipants(plan.participants, record.snapshot.participants)) {
|
|
724
|
+
throw new BrokerStateError("invalid-transition", "Plan must preserve the accepted goal and participant roster");
|
|
725
|
+
}
|
|
726
|
+
return this.mutateCoWork(record, actorId, (snapshot) => {
|
|
727
|
+
snapshot.epoch = plan.epoch;
|
|
728
|
+
snapshot.phase = "planning";
|
|
729
|
+
snapshot.plan = clone(plan);
|
|
730
|
+
snapshot.planHash = canonicalPlanHash(plan);
|
|
731
|
+
snapshot.planAcceptedParticipantIds = [];
|
|
732
|
+
snapshot.readyParticipantIds = [];
|
|
733
|
+
snapshot.completedParticipantIds = [];
|
|
734
|
+
snapshot.completionAssertions = [];
|
|
735
|
+
snapshot.integration = null;
|
|
736
|
+
return "PLAN";
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
acceptCoWorkPlan(actorId, token) {
|
|
740
|
+
const record = this.requireCoWork(token.coWorkId);
|
|
741
|
+
this.assertPlanToken(record.snapshot, token);
|
|
742
|
+
this.requireRequiredParticipant(record.snapshot, actorId);
|
|
743
|
+
if (record.snapshot.phase === "prepared" && record.snapshot.planAcceptedParticipantIds.includes(actorId)) {
|
|
744
|
+
return this.transition(record.snapshot, actorId);
|
|
745
|
+
}
|
|
746
|
+
if (record.snapshot.phase !== "planning" || record.snapshot.plan === null) {
|
|
747
|
+
throw new BrokerStateError("invalid-transition", "Co-work plan is not awaiting acceptance");
|
|
748
|
+
}
|
|
749
|
+
return this.mutateCoWork(record, actorId, (snapshot) => {
|
|
750
|
+
snapshot.planAcceptedParticipantIds = appendUnique(snapshot.planAcceptedParticipantIds, actorId);
|
|
751
|
+
if (includesAll(snapshot.planAcceptedParticipantIds, this.requiredIds(snapshot))) snapshot.phase = "prepared";
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
markCoWorkReady(actorId, token) {
|
|
755
|
+
const record = this.requireCoWork(token.coWorkId);
|
|
756
|
+
this.assertPlanToken(record.snapshot, token);
|
|
757
|
+
this.requireRequiredParticipant(record.snapshot, actorId);
|
|
758
|
+
if (record.snapshot.phase === "running" && record.snapshot.readyParticipantIds.includes(actorId)) {
|
|
759
|
+
return this.transition(record.snapshot, actorId);
|
|
760
|
+
}
|
|
761
|
+
if (record.snapshot.phase !== "planning" && record.snapshot.phase !== "prepared") {
|
|
762
|
+
throw new BrokerStateError("invalid-transition", "Co-work is not accepting readiness");
|
|
763
|
+
}
|
|
764
|
+
if (!record.snapshot.planAcceptedParticipantIds.includes(actorId)) {
|
|
765
|
+
throw new BrokerStateError("invalid-transition", "Participant must accept the current plan before becoming ready");
|
|
766
|
+
}
|
|
767
|
+
return this.mutateCoWork(record, actorId, (snapshot) => {
|
|
768
|
+
snapshot.readyParticipantIds = appendUnique(snapshot.readyParticipantIds, actorId);
|
|
769
|
+
if (includesAll(snapshot.planAcceptedParticipantIds, this.requiredIds(snapshot)) && includesAll(snapshot.readyParticipantIds, this.requiredIds(snapshot))) {
|
|
770
|
+
snapshot.phase = "running";
|
|
771
|
+
return "START";
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
completeCoWork(actorId, input) {
|
|
776
|
+
const record = this.requireCoWork(input.coWorkId);
|
|
777
|
+
this.assertPlanToken(record.snapshot, input);
|
|
778
|
+
this.requireRequiredParticipant(record.snapshot, actorId);
|
|
779
|
+
if (record.snapshot.phase !== "running") throw new BrokerStateError("invalid-transition", "Co-work is not running");
|
|
780
|
+
if (!record.snapshot.acceptedParticipantIds.includes(actorId) || !record.snapshot.readyParticipantIds.includes(actorId)) {
|
|
781
|
+
throw new BrokerStateError("invalid-transition", "Participant must be accepted and ready before completing co-work");
|
|
782
|
+
}
|
|
783
|
+
if (record.snapshot.completionAssertions.some(({ participantId }) => participantId === actorId)) {
|
|
784
|
+
return this.transition(record.snapshot, actorId);
|
|
785
|
+
}
|
|
786
|
+
return this.mutateCoWork(record, actorId, (snapshot) => {
|
|
787
|
+
snapshot.completionAssertions.push(input.detail === void 0 ? { participantId: actorId, passed: input.passed } : { participantId: actorId, passed: input.passed, detail: input.detail });
|
|
788
|
+
if (!input.passed) {
|
|
789
|
+
snapshot.phase = "failed";
|
|
790
|
+
return "FAIL";
|
|
791
|
+
}
|
|
792
|
+
snapshot.completedParticipantIds = appendUnique(snapshot.completedParticipantIds, actorId);
|
|
793
|
+
if (includesAll(snapshot.completedParticipantIds, this.requiredIds(snapshot))) snapshot.phase = "verifying";
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
integrateCoWork(actorId, input) {
|
|
797
|
+
const record = this.requireCoWork(input.coWorkId);
|
|
798
|
+
this.assertPlanToken(record.snapshot, input);
|
|
799
|
+
this.requireRequiredParticipant(record.snapshot, actorId);
|
|
800
|
+
if (actorId !== record.snapshot.integrationOwnerId) {
|
|
801
|
+
throw new BrokerStateError("invalid-transition", "Only the integration owner can finalize co-work integration");
|
|
802
|
+
}
|
|
803
|
+
if (record.snapshot.phase !== "verifying") throw new BrokerStateError("invalid-transition", "Co-work is not ready for integration verification");
|
|
804
|
+
return this.mutateCoWork(record, actorId, (snapshot) => {
|
|
805
|
+
snapshot.integration = { passed: input.passed, evidence: input.evidence };
|
|
806
|
+
snapshot.phase = input.passed ? "completed" : "failed";
|
|
807
|
+
return input.passed ? "END" : "FAIL";
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
getCoWork(coWorkId) {
|
|
811
|
+
return clone(this.requireCoWork(coWorkId).snapshot);
|
|
812
|
+
}
|
|
813
|
+
cancelCoWork(actorId, coWorkId, _reason) {
|
|
814
|
+
const record = this.requireCoWork(coWorkId);
|
|
815
|
+
this.requireRequiredParticipant(record.snapshot, actorId);
|
|
816
|
+
if (record.snapshot.phase === "completed" || record.snapshot.phase === "failed" || record.snapshot.phase === "cancelled") {
|
|
817
|
+
throw new BrokerStateError("invalid-transition", "Co-work is already terminal");
|
|
818
|
+
}
|
|
819
|
+
return this.mutateCoWork(record, actorId, (snapshot) => {
|
|
820
|
+
snapshot.phase = "cancelled";
|
|
821
|
+
return "CANCEL";
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
sweepExpired() {
|
|
825
|
+
const cutoff = this.now() - this.heartbeatTimeoutMs;
|
|
826
|
+
for (const [id, record] of this.pals) {
|
|
827
|
+
if (record.lastSeenMs < cutoff) this.disconnect(id);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
requirePal(palId) {
|
|
831
|
+
this.sweepExpired();
|
|
832
|
+
const record = this.pals.get(palId.toLowerCase());
|
|
833
|
+
if (record === void 0) throw new BrokerStateError("not-found", `Pal '${palId}' is not active`);
|
|
834
|
+
return record.presence;
|
|
835
|
+
}
|
|
836
|
+
duplicateReceipt(messageId) {
|
|
837
|
+
const recipientIds = this.delivered.get(messageId);
|
|
838
|
+
if (recipientIds === void 0) return void 0;
|
|
839
|
+
return DeliveryReceiptSchema.parse({
|
|
840
|
+
version: 1,
|
|
841
|
+
type: "delivery-receipt",
|
|
842
|
+
messageId,
|
|
843
|
+
status: "duplicate",
|
|
844
|
+
recipientIds
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
rememberDelivery(messageId, recipientIds) {
|
|
848
|
+
this.delivered.set(messageId, [...recipientIds]);
|
|
849
|
+
while (this.delivered.size > this.dedupLimit) {
|
|
850
|
+
const oldest = this.delivered.keys().next().value;
|
|
851
|
+
if (oldest === void 0) break;
|
|
852
|
+
this.delivered.delete(oldest);
|
|
853
|
+
}
|
|
854
|
+
return DeliveryReceiptSchema.parse({
|
|
855
|
+
version: 1,
|
|
856
|
+
type: "delivery-receipt",
|
|
857
|
+
messageId,
|
|
858
|
+
status: "delivered",
|
|
859
|
+
recipientIds
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
requireCoWork(coWorkId) {
|
|
863
|
+
const record = this.coWorks.get(coWorkId);
|
|
864
|
+
if (record === void 0) throw new BrokerStateError("not-found", `Co-work '${coWorkId}' does not exist`);
|
|
865
|
+
return record;
|
|
866
|
+
}
|
|
867
|
+
requireParticipant(snapshot, palId) {
|
|
868
|
+
if (!snapshot.participants.some((participant) => participant.palId === palId)) {
|
|
869
|
+
throw new BrokerStateError("invalid-transition", `Pal '${palId}' is not a co-work participant`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
requireRequiredParticipant(snapshot, palId) {
|
|
873
|
+
this.requireParticipant(snapshot, palId);
|
|
874
|
+
if (!snapshot.participants.some((participant) => participant.palId === palId && participant.required)) {
|
|
875
|
+
throw new BrokerStateError("invalid-transition", `Pal '${palId}' is an observer and cannot change co-work state`);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
requiredIds(snapshot) {
|
|
879
|
+
return snapshot.participants.filter(({ required }) => required).map(({ palId }) => palId);
|
|
880
|
+
}
|
|
881
|
+
assertEpoch(snapshot, epoch) {
|
|
882
|
+
if (epoch !== snapshot.epoch) throw new BrokerStateError("stale-epoch", `Expected epoch ${snapshot.epoch}`);
|
|
883
|
+
}
|
|
884
|
+
assertPlanToken(snapshot, token) {
|
|
885
|
+
this.assertEpoch(snapshot, token.epoch);
|
|
886
|
+
if (snapshot.planHash === null || token.planHash !== snapshot.planHash) {
|
|
887
|
+
throw new BrokerStateError("invalid-transition", "Plan hash does not match the canonical active plan");
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
transition(snapshot, actorId, action) {
|
|
891
|
+
const result = CoWorkSnapshotSchema.parse(clone(snapshot));
|
|
892
|
+
if (action === void 0) return { snapshot: result, events: [] };
|
|
893
|
+
return {
|
|
894
|
+
snapshot: result,
|
|
895
|
+
events: [{
|
|
896
|
+
version: 1,
|
|
897
|
+
type: "cowork-event",
|
|
898
|
+
action,
|
|
899
|
+
actorId,
|
|
900
|
+
coWorkId: result.coWorkId,
|
|
901
|
+
epoch: result.epoch,
|
|
902
|
+
planHash: result.planHash,
|
|
903
|
+
snapshot: clone(result)
|
|
904
|
+
}]
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
mutateCoWork(record, actorId, mutate) {
|
|
908
|
+
const candidate = clone(record.snapshot);
|
|
909
|
+
const action = mutate(candidate);
|
|
910
|
+
const committed = CoWorkSnapshotSchema.parse(candidate);
|
|
911
|
+
record.snapshot = committed;
|
|
912
|
+
return this.transition(committed, actorId, action);
|
|
913
|
+
}
|
|
914
|
+
coWorkEvictionCandidate() {
|
|
915
|
+
if (this.coWorks.size < this.maxCoWorks) return void 0;
|
|
916
|
+
const terminal = [...this.coWorks.entries()].find(([, { snapshot }]) => snapshot.phase === "completed" || snapshot.phase === "failed" || snapshot.phase === "cancelled");
|
|
917
|
+
if (terminal === void 0) throw new BrokerStateError("capacity", "Active co-work capacity reached");
|
|
918
|
+
return terminal[0];
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
// src/pals/broker-cli.ts
|
|
923
|
+
var startupAttempts = /* @__PURE__ */ new Map();
|
|
924
|
+
function delay(milliseconds) {
|
|
925
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
926
|
+
}
|
|
927
|
+
function canConnect2(address) {
|
|
928
|
+
return new Promise((resolve) => {
|
|
929
|
+
const socket = new Socket2();
|
|
930
|
+
const finish = (connected) => {
|
|
931
|
+
socket.removeAllListeners();
|
|
932
|
+
socket.destroy();
|
|
933
|
+
resolve(connected);
|
|
934
|
+
};
|
|
935
|
+
socket.once("connect", () => finish(true));
|
|
936
|
+
socket.once("error", () => finish(false));
|
|
937
|
+
socket.connect(address);
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
function startupLockPath(address) {
|
|
941
|
+
const digest = createHash2("sha256").update(address, "utf8").digest("hex").slice(0, 24);
|
|
942
|
+
return join2(tmpdir(), `flavor-code-pals-${digest}.startup.lock`);
|
|
943
|
+
}
|
|
944
|
+
async function spawnProductionBroker(address) {
|
|
945
|
+
const entry = process.argv[1];
|
|
946
|
+
if (entry === void 0) throw new Error("Cannot auto-start pals broker without a CLI entry point");
|
|
947
|
+
return spawnPalBrokerProcess({ address, executable: process.execPath, args: [entry, "--pals-broker", address] });
|
|
948
|
+
}
|
|
949
|
+
async function spawnPalBrokerProcess(options) {
|
|
950
|
+
if (options.executable.trim() === "" || options.args.length === 0) throw new Error("Pals broker process requires an executable and entry arguments");
|
|
951
|
+
const address = options.address;
|
|
952
|
+
const lockPath = startupLockPath(address);
|
|
953
|
+
const lock = await acquirePalFileLock({ path: lockPath, endpointLive: () => canConnect2(address) });
|
|
954
|
+
if (lock === void 0) return;
|
|
955
|
+
try {
|
|
956
|
+
const child = spawn(options.executable, [...options.args], {
|
|
957
|
+
detached: true,
|
|
958
|
+
stdio: "ignore",
|
|
959
|
+
windowsHide: true,
|
|
960
|
+
...options.environment === void 0 ? {} : { env: options.environment }
|
|
961
|
+
});
|
|
962
|
+
child.unref();
|
|
963
|
+
const deadline = Date.now() + 5e3;
|
|
964
|
+
while (Date.now() < deadline && !await canConnect2(address)) await delay(25);
|
|
965
|
+
if (!await canConnect2(address)) throw new Error(`Pals broker process did not become ready at '${address}'`);
|
|
966
|
+
} finally {
|
|
967
|
+
await lock.release();
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async function runPalBroker(options) {
|
|
971
|
+
const server = new PalBrokerServer(options);
|
|
972
|
+
await server.start();
|
|
973
|
+
const sweepIntervalMs = options.sweepIntervalMs ?? 5e3;
|
|
974
|
+
const idleShutdownMs = options.idleShutdownMs ?? 3e4;
|
|
975
|
+
let resolveClosed;
|
|
976
|
+
const closed = new Promise((resolve) => {
|
|
977
|
+
resolveClosed = resolve;
|
|
978
|
+
});
|
|
979
|
+
let closing;
|
|
980
|
+
const timer = setInterval(() => {
|
|
981
|
+
server.sweep();
|
|
982
|
+
if (server.connectionCount === 0 && Date.now() - server.lastActivityAt >= idleShutdownMs) void close();
|
|
983
|
+
}, sweepIntervalMs);
|
|
984
|
+
timer.unref?.();
|
|
985
|
+
const close = () => {
|
|
986
|
+
if (closing !== void 0) return closing;
|
|
987
|
+
closing = (async () => {
|
|
988
|
+
clearInterval(timer);
|
|
989
|
+
await server.close();
|
|
990
|
+
resolveClosed();
|
|
991
|
+
})();
|
|
992
|
+
return closing;
|
|
993
|
+
};
|
|
994
|
+
return { server, closed, close };
|
|
995
|
+
}
|
|
996
|
+
function ensurePalBrokerRunning(options) {
|
|
997
|
+
const existing = startupAttempts.get(options.address);
|
|
998
|
+
if (existing !== void 0) return existing;
|
|
999
|
+
const attempt = ensureOnce(options).finally(() => startupAttempts.delete(options.address));
|
|
1000
|
+
startupAttempts.set(options.address, attempt);
|
|
1001
|
+
return attempt;
|
|
1002
|
+
}
|
|
1003
|
+
async function ensureOnce(options) {
|
|
1004
|
+
if (await canConnect2(options.address)) return;
|
|
1005
|
+
await (options.startBroker ?? (() => spawnProductionBroker(options.address)))();
|
|
1006
|
+
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
1007
|
+
const retryDelayMs = options.retryDelayMs ?? 25;
|
|
1008
|
+
const deadline = Date.now() + timeoutMs;
|
|
1009
|
+
do {
|
|
1010
|
+
if (await canConnect2(options.address)) return;
|
|
1011
|
+
await delay(retryDelayMs);
|
|
1012
|
+
} while (Date.now() < deadline);
|
|
1013
|
+
throw new Error(`Timed out waiting for pals broker at '${options.address}'`);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export {
|
|
1017
|
+
loadOrCreatePalAuthToken,
|
|
1018
|
+
validatePalAuthToken,
|
|
1019
|
+
spawnPalBrokerProcess,
|
|
1020
|
+
runPalBroker,
|
|
1021
|
+
ensurePalBrokerRunning
|
|
1022
|
+
};
|