@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.19.1-fork.1
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/CHANGELOG.md +135 -0
- package/dist/src/agent-execution.js +114 -87
- package/dist/src/background-widget.d.ts +61 -0
- package/dist/src/background-widget.js +23 -16
- package/dist/src/decoders.js +28 -1
- package/dist/src/execution.d.ts +4 -2
- package/dist/src/execution.js +43 -2
- package/dist/src/host-delivery.js +1 -0
- package/dist/src/host-navigator.d.ts +18 -0
- package/dist/src/host-navigator.js +71 -13
- package/dist/src/host-recovery.d.ts +5 -2
- package/dist/src/host-recovery.js +3 -3
- package/dist/src/host-view.d.ts +21 -1
- package/dist/src/host-view.js +138 -47
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +246 -60
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +1346 -622
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/roles.d.ts +70 -0
- package/dist/src/roles.js +326 -0
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/settings.d.ts +26 -0
- package/dist/src/settings.js +277 -0
- package/dist/src/trajectory.d.ts +39 -4
- package/dist/src/trajectory.js +82 -5
- package/dist/src/types.d.ts +30 -3
- package/dist/src/types.js +1 -1
- package/dist/src/utils.d.ts +21 -1
- package/dist/src/utils.js +40 -0
- package/dist/src/validation.d.ts +10 -17
- package/dist/src/validation.js +29 -458
- package/dist/starter/index.js +0 -2
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +0 -2
- package/dist/subagents/index.js +1344 -798
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +49 -29
- package/dist/subagents/src/contracts.js +18 -7
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +15 -7
- package/dist/subagents/src/manager.js +32 -13
- package/dist/subagents/src/navigator.js +396 -247
- package/dist/subagents/src/view.d.ts +24 -2
- package/dist/subagents/src/view.js +107 -80
- package/dist/trajectory/assets/index.html +370 -232
- package/dist/trajectory/assets/prism.min.js +13 -0
- package/dist/trajectory/index.js +445 -277
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +370 -232
- package/dist/trajectory/src/assets/prism.min.js +13 -0
- package/dist/trajectory/src/export.js +4 -2
- package/dist/trajectory/src/index.d.ts +2 -0
- package/dist/trajectory/src/index.js +151 -60
- package/dist/trajectory/src/server.js +138 -11
- package/dist/trajectory/src/server.js.map +2 -2
- package/package.json +7 -6
- package/skills/pi-extensible-workflows/SKILL.md +2 -0
- package/src/agent-execution.ts +110 -80
- package/src/background-widget.ts +26 -19
- package/src/decoders.ts +22 -1
- package/src/execution.ts +41 -4
- package/src/host-delivery.ts +1 -0
- package/src/host-navigator.ts +58 -14
- package/src/host-recovery.ts +8 -7
- package/src/host-view.ts +116 -42
- package/src/host.ts +223 -55
- package/src/index.ts +3 -1
- package/src/paths.ts +3 -0
- package/src/registry.ts +3 -2
- package/src/roles.ts +327 -0
- package/src/script-tools.ts +76 -0
- package/src/settings.ts +224 -0
- package/src/trajectory.ts +97 -9
- package/src/types.ts +12 -5
- package/src/utils.ts +36 -1
- package/src/validation.ts +30 -356
- package/starter/review-loop.ts +0 -2
- package/subagents/README.md +3 -3
- package/subagents/src/contracts.ts +22 -7
- package/subagents/src/index.ts +18 -7
- package/subagents/src/manager.ts +31 -13
- package/subagents/src/navigator.ts +336 -180
- package/subagents/src/view.ts +108 -76
- package/trajectory/src/assets/index.html +370 -232
- package/trajectory/src/assets/prism.min.js +13 -0
- package/trajectory/src/export.ts +4 -2
- package/trajectory/src/index.ts +124 -47
- package/trajectory/src/server.ts +114 -14
package/trajectory/src/server.ts
CHANGED
|
@@ -5,20 +5,21 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { URL } from "node:url";
|
|
6
6
|
import { isTrajectoryAction, isTrajectoryTarget, trajectoryActionError } from "../../src/trajectory-contracts.js";
|
|
7
7
|
import { sameFilesystemPath } from "../../src/paths.js";
|
|
8
|
+
import { TOOL_TIMING_ENTRY_TYPE } from "../../src/tool-timing.js";
|
|
8
9
|
const TRAJECTORY_IDLE_EXIT_MS = 5 * 60 * 1000;
|
|
9
10
|
|
|
10
11
|
type Socket = import("node:stream").Duplex;
|
|
11
12
|
type ClientKind = "publisher" | "browser";
|
|
12
|
-
type
|
|
13
|
+
type RunFocus = { publisherId: string; runId: string };
|
|
14
|
+
type Client = { socket: Socket; kind: ClientKind; publisherId?: string; buffer: Buffer; pendingState: Buffer | undefined; pendingDeliver: (() => void) | undefined; backpressured: boolean; superseded: boolean; focusAware?: boolean; focus?: RunFocus; sentTiming?: Map<string, number> };
|
|
13
15
|
type State = { type: "state"; publishers: readonly unknown[]; updatedAt: number; initial?: boolean; truncated?: boolean };
|
|
14
16
|
type PendingRequest = { requestId: string; kind: "transcript" | "action"; browser: Client; publisherId: string; publisher: Client; target: { runId?: string; agentId?: string; subagentId?: string; revision?: number }; timer: ReturnType<typeof setTimeout> };
|
|
15
17
|
const MAX_FRAME_BYTES = 32 * 1024 * 1024;
|
|
16
18
|
const MAX_PENDING_REQUESTS = 128;
|
|
17
19
|
const TRANSCRIPT_REQUEST_TIMEOUT_MS = 10_000;
|
|
18
20
|
const ACTION_REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
19
|
-
const TIMING_ENTRY_TYPE = "pi-workflows:tool-timing";
|
|
20
21
|
function isTimingEntry(value: unknown): boolean {
|
|
21
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value) && (value as { type?: unknown }).type === "custom" && (value as { customType?: unknown }).customType ===
|
|
22
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && (value as { type?: unknown }).type === "custom" && (value as { customType?: unknown }).customType === TOOL_TIMING_ENTRY_TYPE);
|
|
22
23
|
}
|
|
23
24
|
function compactTranscript(value: unknown): unknown {
|
|
24
25
|
if (Array.isArray(value)) return value.filter(isTimingEntry);
|
|
@@ -39,6 +40,55 @@ function compactSubagent(subagent: unknown): unknown {
|
|
|
39
40
|
const record = subagent as { transcript?: unknown };
|
|
40
41
|
return { ...record, transcript: compactTranscript(record.transcript) };
|
|
41
42
|
}
|
|
43
|
+
function withoutTiming(run: unknown): unknown {
|
|
44
|
+
if (!run || typeof run !== "object" || Array.isArray(run)) return run;
|
|
45
|
+
const record = run as { transcripts?: unknown };
|
|
46
|
+
if (!record.transcripts || typeof record.transcripts !== "object" || Array.isArray(record.transcripts)) return run;
|
|
47
|
+
const transcripts: Record<string, unknown> = {};
|
|
48
|
+
for (const [id, entry] of Object.entries(record.transcripts as Record<string, unknown>)) transcripts[id] = entry && typeof entry === "object" && !Array.isArray(entry) ? { ...entry, timing: [] } : entry;
|
|
49
|
+
return { ...record, transcripts };
|
|
50
|
+
}
|
|
51
|
+
/** Timing already delivered at this revision is omitted; the browser keeps the entries it cached. */
|
|
52
|
+
function knownTiming(run: unknown, prefix: string, sent: Map<string, number>, mark: (key: string, revision: number) => void): unknown {
|
|
53
|
+
if (!run || typeof run !== "object" || Array.isArray(run)) return run;
|
|
54
|
+
const record = run as { transcripts?: unknown };
|
|
55
|
+
if (!record.transcripts || typeof record.transcripts !== "object" || Array.isArray(record.transcripts)) return run;
|
|
56
|
+
const transcripts: Record<string, unknown> = {};
|
|
57
|
+
for (const [id, entry] of Object.entries(record.transcripts as Record<string, unknown>)) {
|
|
58
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) { transcripts[id] = entry; continue; }
|
|
59
|
+
const value = entry as { revision?: unknown; timing?: unknown };
|
|
60
|
+
if (typeof value.revision !== "number" || !Array.isArray(value.timing)) { transcripts[id] = entry; continue; }
|
|
61
|
+
if (sent.get(`${prefix}\t${id}`) === value.revision) { const rest = { ...value }; delete rest.timing; transcripts[id] = rest; continue; }
|
|
62
|
+
mark(`${prefix}\t${id}`, value.revision);
|
|
63
|
+
transcripts[id] = entry;
|
|
64
|
+
}
|
|
65
|
+
return { ...record, transcripts };
|
|
66
|
+
}
|
|
67
|
+
function forgetTiming(run: unknown, prefix: string, sent: Map<string, number>): void {
|
|
68
|
+
if (!run || typeof run !== "object" || Array.isArray(run)) return;
|
|
69
|
+
const record = run as { transcripts?: unknown };
|
|
70
|
+
if (!record.transcripts || typeof record.transcripts !== "object" || Array.isArray(record.transcripts)) return;
|
|
71
|
+
for (const id of Object.keys(record.transcripts)) sent.delete(`${prefix}\t${id}`);
|
|
72
|
+
}
|
|
73
|
+
/** Tool-timing only drives the focused run's gantt, so every other run ships without it. */
|
|
74
|
+
function focusedState(state: State, focus: RunFocus | undefined, sent?: Map<string, number>, mark: (key: string, revision: number) => void = () => {}): State {
|
|
75
|
+
return {
|
|
76
|
+
...state,
|
|
77
|
+
publishers: state.publishers.map((publisher) => {
|
|
78
|
+
if (!publisher || typeof publisher !== "object" || Array.isArray(publisher)) return publisher;
|
|
79
|
+
const value = publisher as { id?: unknown; runs?: unknown };
|
|
80
|
+
const runs: readonly unknown[] | undefined = Array.isArray(value.runs) ? value.runs as readonly unknown[] : undefined;
|
|
81
|
+
if (!runs) return publisher;
|
|
82
|
+
const focusedPublisher = focus !== undefined && value.id === focus.publisherId;
|
|
83
|
+
return { ...value, runs: runs.map((run) => {
|
|
84
|
+
const id = run && typeof run === "object" && !Array.isArray(run) ? (run as { run?: { id?: unknown } }).run?.id : undefined;
|
|
85
|
+
const prefix = `${String(value.id)}\t${String(id)}`;
|
|
86
|
+
if (!focusedPublisher || id !== focus.runId) { if (sent) forgetTiming(run, prefix, sent); return withoutTiming(run); }
|
|
87
|
+
return sent ? knownTiming(run, prefix, sent, mark) : run;
|
|
88
|
+
}) };
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
42
92
|
function compactPublishers(publishers: readonly unknown[]): unknown[] {
|
|
43
93
|
return publishers.map((publisher) => {
|
|
44
94
|
if (!publisher || typeof publisher !== "object" || Array.isArray(publisher)) return publisher;
|
|
@@ -77,7 +127,7 @@ function minimalPublisher(publisher: unknown): unknown {
|
|
|
77
127
|
function tinyPublisher(publisher: unknown): unknown {
|
|
78
128
|
if (!publisher || typeof publisher !== "object" || Array.isArray(publisher)) return { id: "publisher" };
|
|
79
129
|
const value = publisher as Record<string, unknown>;
|
|
80
|
-
return { id: typeof value.id === "string" ? value.id.slice(0, 200) : "publisher", title: typeof value.title === "string" ? value.title.slice(0, 200) : undefined, cwd: typeof value.cwd === "string" ? value.cwd.slice(0, 1024) : undefined, sessionId: typeof value.sessionId === "string" ? value.sessionId.slice(0, 200) : undefined,
|
|
130
|
+
return { id: typeof value.id === "string" ? value.id.slice(0, 200) : "publisher", title: typeof value.title === "string" ? value.title.slice(0, 200) : undefined, cwd: typeof value.cwd === "string" ? value.cwd.slice(0, 1024) : undefined, sessionId: typeof value.sessionId === "string" ? value.sessionId.slice(0, 200) : undefined, connected: value.connected === true };
|
|
81
131
|
}
|
|
82
132
|
function encodePublisherList(publishers: readonly unknown[], updatedAt: number, maxBytes: number, initial: boolean, truncated: boolean, projector: (publisher: unknown) => unknown, fallback?: (publisher: unknown) => unknown): { serialized: string; truncated: boolean } {
|
|
83
133
|
const prefix = `{"type":"state","publishers":[`;
|
|
@@ -133,11 +183,13 @@ function isState(value: unknown): value is State {
|
|
|
133
183
|
return Boolean(value && typeof value === "object" && !Array.isArray(value) && (value as { type?: unknown }).type === "state");
|
|
134
184
|
}
|
|
135
185
|
|
|
136
|
-
function writeFrame(client: Client, packet: Buffer, state: boolean): void {
|
|
137
|
-
|
|
186
|
+
function writeFrame(client: Client, packet: Buffer, state: boolean, onDelivered?: () => void): void {
|
|
187
|
+
// A superseded pending frame is never sent, so its payload must not count as delivered.
|
|
188
|
+
if (state && client.backpressured) { client.pendingState = packet; client.pendingDeliver = onDelivered; return; }
|
|
138
189
|
try {
|
|
139
|
-
if (state) client.pendingState = undefined;
|
|
190
|
+
if (state) { client.pendingState = undefined; client.pendingDeliver = undefined; }
|
|
140
191
|
if (!client.socket.write(packet)) client.backpressured = true;
|
|
192
|
+
onDelivered?.();
|
|
141
193
|
} catch { client.socket.destroy(); }
|
|
142
194
|
}
|
|
143
195
|
|
|
@@ -194,6 +246,7 @@ type TrajectoryServerOptions = { maxFrameBytes?: number; fingerprint?: string };
|
|
|
194
246
|
export function createTrajectoryServer(port: number, lockPath: string, options: TrajectoryServerOptions = {}): Server {
|
|
195
247
|
const maxFrameBytes = options.maxFrameBytes ?? MAX_FRAME_BYTES;
|
|
196
248
|
const serverFingerprint = options.fingerprint ?? "";
|
|
249
|
+
let startedAt = 0;
|
|
197
250
|
const clients = new Set<Client>();
|
|
198
251
|
const publishers = new Map<string, { client: Client; value: Record<string, unknown>; generation: number }>();
|
|
199
252
|
let latest: State = { type: "state", publishers: [], updatedAt: Date.now(), initial: true };
|
|
@@ -234,8 +287,19 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
234
287
|
const broadcast = (value: unknown) => {
|
|
235
288
|
if (isState(value)) {
|
|
236
289
|
try {
|
|
237
|
-
|
|
238
|
-
for (const client of clients)
|
|
290
|
+
let shared: Buffer | undefined;
|
|
291
|
+
for (const client of clients) {
|
|
292
|
+
if (client.kind !== "browser") continue;
|
|
293
|
+
if (client.focusAware !== true) {
|
|
294
|
+
shared ??= frame(encodeState(value, maxFrameBytes), maxFrameBytes);
|
|
295
|
+
writeFrame(client, shared, true);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const sent = (client.sentTiming ??= new Map<string, number>());
|
|
299
|
+
const marks: [string, number][] = [];
|
|
300
|
+
const stateFrame = frame(encodeState(focusedState(value, client.focus, sent, (key, revision) => marks.push([key, revision])), maxFrameBytes), maxFrameBytes);
|
|
301
|
+
writeFrame(client, stateFrame, true, () => { for (const [key, revision] of marks) sent.set(key, revision); });
|
|
302
|
+
}
|
|
239
303
|
} catch {
|
|
240
304
|
for (const client of clients) if (client.kind === "browser") client.socket.destroy();
|
|
241
305
|
}
|
|
@@ -243,6 +307,14 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
243
307
|
}
|
|
244
308
|
for (const client of clients) if (client.kind === "browser") emit(client, value);
|
|
245
309
|
};
|
|
310
|
+
let viewers = 0;
|
|
311
|
+
// Publishers poll their run directory, so they only need a fast cadence while a browser is watching.
|
|
312
|
+
const notifyViewers = () => {
|
|
313
|
+
const count = [...clients].filter((candidate) => candidate.kind === "browser").length;
|
|
314
|
+
if (count === viewers) return;
|
|
315
|
+
viewers = count;
|
|
316
|
+
for (const candidate of clients) if (candidate.kind === "publisher") emit(candidate, { type: "publisher:viewers", count });
|
|
317
|
+
};
|
|
246
318
|
const publishState = () => {
|
|
247
319
|
const values = [...publishers.values()].map(({ value, generation }) => ({ ...value, generation }));
|
|
248
320
|
latest = { type: "state", publishers: values, updatedAt: Date.now() };
|
|
@@ -251,6 +323,7 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
251
323
|
const disconnect = (client: Client) => {
|
|
252
324
|
clearPending(client, client.kind === "publisher" ? "Publisher is disconnected" : "Trajectory browser disconnected");
|
|
253
325
|
clients.delete(client);
|
|
326
|
+
if (client.kind === "browser") notifyViewers();
|
|
254
327
|
if (client.kind === "publisher" && client.publisherId && publishers.get(client.publisherId)?.client === client) {
|
|
255
328
|
publishers.delete(client.publisherId);
|
|
256
329
|
publishState();
|
|
@@ -264,7 +337,21 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
264
337
|
const message = value as Record<string, unknown>;
|
|
265
338
|
if (client.superseded) return;
|
|
266
339
|
if (message.type === "publisher:detach" && client.kind === "publisher" && client.publisherId && publishers.get(client.publisherId)?.client === client) { clearPending(client, "Publisher is disconnected"); publishers.delete(client.publisherId); publishState(); scheduleIdleExit(); return; }
|
|
267
|
-
if (message.type === "ui:attach") { client.kind = "browser"; emit(client, latest); return; }
|
|
340
|
+
if (message.type === "ui:attach") { client.kind = "browser"; emit(client, latest); notifyViewers(); return; }
|
|
341
|
+
if (message.type === "ui:focus" && client.kind === "browser") {
|
|
342
|
+
const publisherId = typeof message.publisherId === "string" ? message.publisherId : undefined;
|
|
343
|
+
const runId = typeof message.runId === "string" ? message.runId : undefined;
|
|
344
|
+
client.focusAware = true;
|
|
345
|
+
client.sentTiming = new Map<string, number>();
|
|
346
|
+
if (publisherId !== undefined && runId !== undefined) client.focus = { publisherId, runId };
|
|
347
|
+
else delete client.focus;
|
|
348
|
+
const sent = client.sentTiming;
|
|
349
|
+
const marks: [string, number][] = [];
|
|
350
|
+
const focused = focusedState(latest, client.focus, sent, (key, revision) => marks.push([key, revision]));
|
|
351
|
+
try { writeFrame(client, frame(encodeState(focused, maxFrameBytes), maxFrameBytes), true, () => { for (const [key, revision] of marks) sent.set(key, revision); }); }
|
|
352
|
+
catch { client.socket.destroy(); }
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
268
355
|
if (client.kind === "publisher" && message.type === "publisher:attach" && typeof message.publisherId === "string") {
|
|
269
356
|
cancelIdleExit();
|
|
270
357
|
client.publisherId = message.publisherId;
|
|
@@ -277,6 +364,7 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
277
364
|
}
|
|
278
365
|
const generation = (previous?.generation ?? 0) + 1;
|
|
279
366
|
publishers.set(message.publisherId, { client, value: { ...(previous?.value ?? { id: message.publisherId }), id: message.publisherId, connected: true }, generation });
|
|
367
|
+
emit(client, { type: "publisher:viewers", count: viewers });
|
|
280
368
|
publishState();
|
|
281
369
|
return;
|
|
282
370
|
}
|
|
@@ -374,7 +462,8 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
374
462
|
catch { writeJson(response, 400, { error: "Invalid request" }); return; }
|
|
375
463
|
if (!authorized(request, port)) { writeJson(response, 403, { error: "Forbidden" }); return; }
|
|
376
464
|
const path = url.pathname;
|
|
377
|
-
|
|
465
|
+
// The identity lets an attaching Pi tell its own server from any other one answering on the port.
|
|
466
|
+
if (request.method === "GET" && path === "/health") { writeJson(response, 200, { ok: true, pid: process.pid, fingerprint: serverFingerprint, startedAt }); return; }
|
|
378
467
|
if (request.method === "GET" && (path === "/" || path === "/index.html")) {
|
|
379
468
|
void readFile(new URL("./assets/index.html", import.meta.url)).then((html) => {
|
|
380
469
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8", "content-length": html.byteLength, "cache-control": "no-store" });
|
|
@@ -396,6 +485,13 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
396
485
|
}).catch(() => { writeJson(response, 500, { error: "Trajectory DOM diffing library is unavailable" }); });
|
|
397
486
|
return;
|
|
398
487
|
}
|
|
488
|
+
if (request.method === "GET" && path === "/prism.min.js") {
|
|
489
|
+
void readFile(new URL("./assets/prism.min.js", import.meta.url)).then((script) => {
|
|
490
|
+
response.writeHead(200, { "content-type": "application/javascript; charset=utf-8", "content-length": script.byteLength, "cache-control": "no-store" });
|
|
491
|
+
response.end(script);
|
|
492
|
+
}).catch(() => { writeJson(response, 500, { error: "Trajectory syntax highlighting library is unavailable" }); });
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
399
495
|
if (request.method === "GET" && (path === "/favicon.png" || path === "/favicon.ico")) {
|
|
400
496
|
void readFile(new URL("./assets/favicon.png", import.meta.url)).then((icon) => {
|
|
401
497
|
response.writeHead(200, { "content-type": "image/png", "content-length": icon.byteLength, "cache-control": "no-store" });
|
|
@@ -413,12 +509,14 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
413
509
|
if (!authorized(request, port) || url.pathname !== "/ws" || typeof key !== "string") { socket.destroy(); return; }
|
|
414
510
|
const accept = createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
|
|
415
511
|
socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`);
|
|
416
|
-
const client: Client = { socket, kind: "publisher", buffer: Buffer.alloc(0), pendingState: undefined, backpressured: false, superseded: false };
|
|
512
|
+
const client: Client = { socket, kind: "publisher", buffer: Buffer.alloc(0), pendingState: undefined, pendingDeliver: undefined, backpressured: false, superseded: false };
|
|
417
513
|
socket.on("drain", () => {
|
|
418
514
|
const pendingState = client.pendingState;
|
|
515
|
+
const pendingDeliver = client.pendingDeliver;
|
|
419
516
|
client.pendingState = undefined;
|
|
517
|
+
client.pendingDeliver = undefined;
|
|
420
518
|
client.backpressured = false;
|
|
421
|
-
if (pendingState !== undefined) writeFrame(client, pendingState, true);
|
|
519
|
+
if (pendingState !== undefined) writeFrame(client, pendingState, true, pendingDeliver);
|
|
422
520
|
});
|
|
423
521
|
clients.add(client);
|
|
424
522
|
socket.on("data", (chunk: unknown) => {
|
|
@@ -427,8 +525,10 @@ export function createTrajectoryServer(port: number, lockPath: string, options:
|
|
|
427
525
|
});
|
|
428
526
|
socket.on("close", () => { disconnect(client); });
|
|
429
527
|
socket.on("error", () => { disconnect(client); });
|
|
528
|
+
// A dropped tab only sends FIN, and an upgraded socket stays half-open until this side closes it.
|
|
529
|
+
socket.on("end", () => { socket.destroy(); });
|
|
430
530
|
});
|
|
431
|
-
server.once("listening", () => { void writeFile(lockPath, `${JSON.stringify({ pid: process.pid, port, fingerprint: serverFingerprint, startedAt
|
|
531
|
+
server.once("listening", () => { startedAt = Date.now(); void writeFile(lockPath, `${JSON.stringify({ pid: process.pid, port, fingerprint: serverFingerprint, startedAt })}\n`, { mode: 0o600 }).catch(() => { process.exitCode = 1; }); scheduleIdleExit(); });
|
|
432
532
|
server.on("close", () => { closed = true; if (idleTimer !== undefined) { clearTimeout(idleTimer); idleTimer = undefined; } });
|
|
433
533
|
return server;
|
|
434
534
|
}
|