@chances-ai/wire 24.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/rpc/acp/adapter.d.ts +32 -0
- package/dist/rpc/acp/adapter.d.ts.map +1 -0
- package/dist/rpc/acp/adapter.js +185 -0
- package/dist/rpc/acp/adapter.js.map +1 -0
- package/dist/rpc/acp/engine-driver.d.ts +128 -0
- package/dist/rpc/acp/engine-driver.d.ts.map +1 -0
- package/dist/rpc/acp/engine-driver.js +550 -0
- package/dist/rpc/acp/engine-driver.js.map +1 -0
- package/dist/rpc/acp/event-map.d.ts +22 -0
- package/dist/rpc/acp/event-map.d.ts.map +1 -0
- package/dist/rpc/acp/event-map.js +205 -0
- package/dist/rpc/acp/event-map.js.map +1 -0
- package/dist/rpc/acp/load-sdk.d.ts +3 -0
- package/dist/rpc/acp/load-sdk.d.ts.map +1 -0
- package/dist/rpc/acp/load-sdk.js +24 -0
- package/dist/rpc/acp/load-sdk.js.map +1 -0
- package/dist/rpc/acp/workspace-query.d.ts +41 -0
- package/dist/rpc/acp/workspace-query.d.ts.map +1 -0
- package/dist/rpc/acp/workspace-query.js +89 -0
- package/dist/rpc/acp/workspace-query.js.map +1 -0
- package/dist/rpc/driver.d.ts +42 -0
- package/dist/rpc/driver.d.ts.map +1 -0
- package/dist/rpc/driver.js +7 -0
- package/dist/rpc/driver.js.map +1 -0
- package/dist/rpc/event-map.d.ts +8 -0
- package/dist/rpc/event-map.d.ts.map +1 -0
- package/dist/rpc/event-map.js +91 -0
- package/dist/rpc/event-map.js.map +1 -0
- package/dist/rpc/index.d.ts +13 -0
- package/dist/rpc/index.d.ts.map +1 -0
- package/dist/rpc/index.js +18 -0
- package/dist/rpc/index.js.map +1 -0
- package/dist/rpc/lines.d.ts +2 -0
- package/dist/rpc/lines.d.ts.map +1 -0
- package/dist/rpc/lines.js +24 -0
- package/dist/rpc/lines.js.map +1 -0
- package/dist/rpc/protocol.d.ts +315 -0
- package/dist/rpc/protocol.d.ts.map +1 -0
- package/dist/rpc/protocol.js +70 -0
- package/dist/rpc/protocol.js.map +1 -0
- package/dist/rpc/rpc-server.d.ts +56 -0
- package/dist/rpc/rpc-server.d.ts.map +1 -0
- package/dist/rpc/rpc-server.js +305 -0
- package/dist/rpc/rpc-server.js.map +1 -0
- package/dist/rpc/stdout-guard.d.ts +5 -0
- package/dist/rpc/stdout-guard.d.ts.map +1 -0
- package/dist/rpc/stdout-guard.js +31 -0
- package/dist/rpc/stdout-guard.js.map +1 -0
- package/dist/rpc/writer.d.ts +34 -0
- package/dist/rpc/writer.d.ts.map +1 -0
- package/dist/rpc/writer.js +85 -0
- package/dist/rpc/writer.js.map +1 -0
- package/dist/serve/acp-session-host.d.ts +120 -0
- package/dist/serve/acp-session-host.d.ts.map +1 -0
- package/dist/serve/acp-session-host.js +276 -0
- package/dist/serve/acp-session-host.js.map +1 -0
- package/dist/serve/auth.d.ts +21 -0
- package/dist/serve/auth.d.ts.map +1 -0
- package/dist/serve/auth.js +58 -0
- package/dist/serve/auth.js.map +1 -0
- package/dist/serve/highlight.d.ts +25 -0
- package/dist/serve/highlight.d.ts.map +1 -0
- package/dist/serve/highlight.js +28 -0
- package/dist/serve/highlight.js.map +1 -0
- package/dist/serve/index.d.ts +14 -0
- package/dist/serve/index.d.ts.map +1 -0
- package/dist/serve/index.js +23 -0
- package/dist/serve/index.js.map +1 -0
- package/dist/serve/pairing.d.ts +25 -0
- package/dist/serve/pairing.d.ts.map +1 -0
- package/dist/serve/pairing.js +10 -0
- package/dist/serve/pairing.js.map +1 -0
- package/dist/serve/relay-frames.d.ts +29 -0
- package/dist/serve/relay-frames.d.ts.map +1 -0
- package/dist/serve/relay-frames.js +54 -0
- package/dist/serve/relay-frames.js.map +1 -0
- package/dist/serve/relay.d.ts +146 -0
- package/dist/serve/relay.d.ts.map +1 -0
- package/dist/serve/relay.js +475 -0
- package/dist/serve/relay.js.map +1 -0
- package/dist/serve/replay-hub.d.ts +102 -0
- package/dist/serve/replay-hub.d.ts.map +1 -0
- package/dist/serve/replay-hub.js +176 -0
- package/dist/serve/replay-hub.js.map +1 -0
- package/dist/serve/tls.d.ts +20 -0
- package/dist/serve/tls.d.ts.map +1 -0
- package/dist/serve/tls.js +64 -0
- package/dist/serve/tls.js.map +1 -0
- package/dist/serve/ws-transport.d.ts +64 -0
- package/dist/serve/ws-transport.d.ts.map +1 -0
- package/dist/serve/ws-transport.js +92 -0
- package/dist/serve/ws-transport.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (v19 M2 / docs/6.2 §3.2; v20 M3) `ReplayHub` — the seq-stamping, fan-out
|
|
3
|
+
* `ByteSink` with a bounded replay ring. The engine driver (M3: `AcpEngineDriver`)
|
|
4
|
+
* writes its outbound frames through a `BoundedNdjsonWriter` whose sink is this
|
|
5
|
+
* hub. The hub:
|
|
6
|
+
*
|
|
7
|
+
* 1. stamps a relay-authoritative monotonic `rseq` on every frame (the
|
|
8
|
+
* reconnect cursor — `stampSeq`, a payload-agnostic byte splice);
|
|
9
|
+
* 2. keeps the last N stamped frames in a fixed-capacity ring so a
|
|
10
|
+
* reconnecting client can replay the gap (`replaySince`); a cursor below
|
|
11
|
+
* the ring floor gets a typed `reset` (cold reload — never a silent gap);
|
|
12
|
+
* 3. fans the stamped frame out to EVERY attached socket (no exclusive
|
|
13
|
+
* controller — the goose/OpenHands model), guarding each send so a
|
|
14
|
+
* closed/wedged socket is isolated and never blocks the others or the
|
|
15
|
+
* engine.
|
|
16
|
+
*
|
|
17
|
+
* Session state (`busy` + open `request_permission` ids) is read straight off
|
|
18
|
+
* the `AcpEngineDriver` (authoritative) by `AcpSessionHost`, NOT snooped from the
|
|
19
|
+
* fan-out bytes — so the hub stays a pure payload-agnostic seq/replay/fan-out
|
|
20
|
+
* layer. (A byte-snoop lived here in the chances-rpc era; it was removed with the
|
|
21
|
+
* M3 hard cutover, since ACP frames' first key is `rseq`/`jsonrpc`, never `type`.)
|
|
22
|
+
*
|
|
23
|
+
* **The engine never blocks on a slow remote.** `write()` ALWAYS returns `true`:
|
|
24
|
+
* the ring is the durable buffer, so there is no reason to park the writer (and
|
|
25
|
+
* parking on the slowest of many fan-out sockets would let one stalled tab
|
|
26
|
+
* throttle everyone — the exact failure goose avoids by never blocking the
|
|
27
|
+
* producer and dropping a lagging subscriber instead). A socket that stops
|
|
28
|
+
* draining is dropped past a hard byte cap; it reconnects and replays from the
|
|
29
|
+
* ring. JS's single thread makes seq-assign + ring-push naturally atomic — no
|
|
30
|
+
* lock (unlike goose's Rust `Mutex`).
|
|
31
|
+
*
|
|
32
|
+
* Socket-free by construction (fake {@link ServerSocket}s in tests).
|
|
33
|
+
*/
|
|
34
|
+
import { type ByteSink } from "../rpc/index.js";
|
|
35
|
+
import type { ServerSocket } from "./ws-transport.js";
|
|
36
|
+
/** Default ring depth (frames). Generous vs goose's 512 / claude-code's 5000;
|
|
37
|
+
* a normal turn is well under this, so a transient drop replays in full. */
|
|
38
|
+
export declare const DEFAULT_RING_CAPACITY = 4096;
|
|
39
|
+
export interface ReplayHubOptions {
|
|
40
|
+
/** Ring depth in frames. Default {@link DEFAULT_RING_CAPACITY}. */
|
|
41
|
+
capacity?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface ReplaySlice {
|
|
44
|
+
/** True ⇒ the client's cursor predates the ring floor; it must discard its
|
|
45
|
+
* transcript and take `frames` (the whole live ring) as a cold reload. */
|
|
46
|
+
reset: boolean;
|
|
47
|
+
/** The stamped frame lines to replay, in ascending `rseq` order. */
|
|
48
|
+
frames: string[];
|
|
49
|
+
}
|
|
50
|
+
export declare class ReplayHub implements ByteSink {
|
|
51
|
+
private readonly capacity;
|
|
52
|
+
/** Fixed-size circular buffer of stamped frames (O(1) push + evict-oldest, no
|
|
53
|
+
* per-frame `Array.shift`). `start` is the oldest slot; `count` ≤ capacity. */
|
|
54
|
+
private readonly buf;
|
|
55
|
+
private start;
|
|
56
|
+
private count;
|
|
57
|
+
private counter;
|
|
58
|
+
private readonly sockets;
|
|
59
|
+
private drainWaiters;
|
|
60
|
+
constructor(opts?: ReplayHubOptions);
|
|
61
|
+
/**
|
|
62
|
+
* Stamp + ring + fan-out. ALWAYS returns `true` — the engine/writer
|
|
63
|
+
* never parks on a slow remote (the ring is the durable buffer). Receives the
|
|
64
|
+
* writer's `JSON.stringify(frame) + "\n"`; `stampSeq` splices `rseq` after the
|
|
65
|
+
* leading `{`, leaving the trailing newline untouched.
|
|
66
|
+
*/
|
|
67
|
+
write(chunk: string): boolean;
|
|
68
|
+
/** Dormant in M2: `write` never returns `false`, so the writer never awaits a
|
|
69
|
+
* drain. Kept faithful to the `ByteSink` contract (and woken by
|
|
70
|
+
* {@link signalDrain}) in case a future path parks. */
|
|
71
|
+
once(_event: "drain", listener: () => void): void;
|
|
72
|
+
/** Release any (dormant) parked writer — e.g. a socket became writable. */
|
|
73
|
+
signalDrain(): void;
|
|
74
|
+
private ringPush;
|
|
75
|
+
/** The highest `rseq` assigned (0 ⇒ nothing written yet). */
|
|
76
|
+
get headSeq(): number;
|
|
77
|
+
/** The oldest `rseq` still replayable (0 ⇒ ring empty). */
|
|
78
|
+
get floorSeq(): number;
|
|
79
|
+
/**
|
|
80
|
+
* Frames a client at `lastSeq` has missed. `reset` ⇒ its cursor is below the
|
|
81
|
+
* ring floor (unrecoverable) — the caller cold-reloads it with the whole ring.
|
|
82
|
+
* Mirrors goose `subscribe`: `last_id > 0 && floor > 0 && last_id < floor`
|
|
83
|
+
* ⇒ too far behind.
|
|
84
|
+
*/
|
|
85
|
+
replaySince(lastSeq: number): ReplaySlice;
|
|
86
|
+
/** Add a socket to the live fan-out set. Caller (SessionHost) has already sent
|
|
87
|
+
* the welcome + replay synchronously, so the first live frame this socket
|
|
88
|
+
* sees is strictly newer than everything replayed (disjoint, in order). */
|
|
89
|
+
addSocket(socket: ServerSocket): void;
|
|
90
|
+
removeSocket(socket: ServerSocket): void;
|
|
91
|
+
get socketCount(): number;
|
|
92
|
+
/**
|
|
93
|
+
* Send one stamped frame to a single socket, guarded: a dead socket
|
|
94
|
+
* (`send===0`) or a wedged one (buffer past the hard cap) is dropped + closed,
|
|
95
|
+
* never throwing, never blocking the others. Returns whether the socket stays
|
|
96
|
+
* attached. Reused by {@link SessionHost} for the synchronous replay path so
|
|
97
|
+
* replay + live share one robust send.
|
|
98
|
+
*/
|
|
99
|
+
sendTo(socket: ServerSocket, line: string): boolean;
|
|
100
|
+
private fanout;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=replay-hub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-hub.d.ts","sourceRoot":"","sources":["../../src/serve/replay-hub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD;6EAC6E;AAC7E,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAY1C,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B;+EAC2E;IAC3E,KAAK,EAAE,OAAO,CAAC;IACf,oEAAoE;IACpE,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,qBAAa,SAAU,YAAW,QAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC;oFACgF;IAChF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA2B;IAC/C,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,KAAK,CAAK;IAElB,OAAO,CAAC,OAAO,CAAK;IAEpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IACnD,OAAO,CAAC,YAAY,CAAyB;gBAEjC,IAAI,GAAE,gBAAqB;IAOvC;;;;;OAKG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAQ7B;;4DAEwD;IACxD,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAIjD,2EAA2E;IAC3E,WAAW,IAAI,IAAI;IAQnB,OAAO,CAAC,QAAQ;IAWhB,6DAA6D;IAC7D,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,2DAA2D;IAC3D,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED;;;;;OAKG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW;IAezC;;gFAE4E;IAC5E,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAIrC,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAIxC,IAAI,WAAW,IAAI,MAAM,CAExB;IAED;;;;;;OAMG;IACH,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAyBnD,OAAO,CAAC,MAAM;CAKf"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (v19 M2 / docs/6.2 §3.2; v20 M3) `ReplayHub` — the seq-stamping, fan-out
|
|
3
|
+
* `ByteSink` with a bounded replay ring. The engine driver (M3: `AcpEngineDriver`)
|
|
4
|
+
* writes its outbound frames through a `BoundedNdjsonWriter` whose sink is this
|
|
5
|
+
* hub. The hub:
|
|
6
|
+
*
|
|
7
|
+
* 1. stamps a relay-authoritative monotonic `rseq` on every frame (the
|
|
8
|
+
* reconnect cursor — `stampSeq`, a payload-agnostic byte splice);
|
|
9
|
+
* 2. keeps the last N stamped frames in a fixed-capacity ring so a
|
|
10
|
+
* reconnecting client can replay the gap (`replaySince`); a cursor below
|
|
11
|
+
* the ring floor gets a typed `reset` (cold reload — never a silent gap);
|
|
12
|
+
* 3. fans the stamped frame out to EVERY attached socket (no exclusive
|
|
13
|
+
* controller — the goose/OpenHands model), guarding each send so a
|
|
14
|
+
* closed/wedged socket is isolated and never blocks the others or the
|
|
15
|
+
* engine.
|
|
16
|
+
*
|
|
17
|
+
* Session state (`busy` + open `request_permission` ids) is read straight off
|
|
18
|
+
* the `AcpEngineDriver` (authoritative) by `AcpSessionHost`, NOT snooped from the
|
|
19
|
+
* fan-out bytes — so the hub stays a pure payload-agnostic seq/replay/fan-out
|
|
20
|
+
* layer. (A byte-snoop lived here in the chances-rpc era; it was removed with the
|
|
21
|
+
* M3 hard cutover, since ACP frames' first key is `rseq`/`jsonrpc`, never `type`.)
|
|
22
|
+
*
|
|
23
|
+
* **The engine never blocks on a slow remote.** `write()` ALWAYS returns `true`:
|
|
24
|
+
* the ring is the durable buffer, so there is no reason to park the writer (and
|
|
25
|
+
* parking on the slowest of many fan-out sockets would let one stalled tab
|
|
26
|
+
* throttle everyone — the exact failure goose avoids by never blocking the
|
|
27
|
+
* producer and dropping a lagging subscriber instead). A socket that stops
|
|
28
|
+
* draining is dropped past a hard byte cap; it reconnects and replays from the
|
|
29
|
+
* ring. JS's single thread makes seq-assign + ring-push naturally atomic — no
|
|
30
|
+
* lock (unlike goose's Rust `Mutex`).
|
|
31
|
+
*
|
|
32
|
+
* Socket-free by construction (fake {@link ServerSocket}s in tests).
|
|
33
|
+
*/
|
|
34
|
+
import { stampSeq } from "../rpc/index.js";
|
|
35
|
+
/** Default ring depth (frames). Generous vs goose's 512 / claude-code's 5000;
|
|
36
|
+
* a normal turn is well under this, so a transient drop replays in full. */
|
|
37
|
+
export const DEFAULT_RING_CAPACITY = 4096;
|
|
38
|
+
/** Drop a socket whose own send buffer grows past this (a wedged reader). 16 MiB
|
|
39
|
+
* is far above any loopback steady state — it only catches a truly stuck client
|
|
40
|
+
* so relay memory stays bounded. The dropped client reconnects + replays. */
|
|
41
|
+
const WEDGED_SOCKET_BYTES = 16 * 1024 * 1024;
|
|
42
|
+
export class ReplayHub {
|
|
43
|
+
capacity;
|
|
44
|
+
/** Fixed-size circular buffer of stamped frames (O(1) push + evict-oldest, no
|
|
45
|
+
* per-frame `Array.shift`). `start` is the oldest slot; `count` ≤ capacity. */
|
|
46
|
+
buf;
|
|
47
|
+
start = 0;
|
|
48
|
+
count = 0;
|
|
49
|
+
counter = 0; // last assigned rseq (monotonic, from 1)
|
|
50
|
+
sockets = new Set();
|
|
51
|
+
drainWaiters = [];
|
|
52
|
+
constructor(opts = {}) {
|
|
53
|
+
this.capacity = Math.max(1, opts.capacity ?? DEFAULT_RING_CAPACITY);
|
|
54
|
+
this.buf = new Array(this.capacity);
|
|
55
|
+
}
|
|
56
|
+
// -- ByteSink (the BoundedNdjsonWriter writes through this) -----------------
|
|
57
|
+
/**
|
|
58
|
+
* Stamp + ring + fan-out. ALWAYS returns `true` — the engine/writer
|
|
59
|
+
* never parks on a slow remote (the ring is the durable buffer). Receives the
|
|
60
|
+
* writer's `JSON.stringify(frame) + "\n"`; `stampSeq` splices `rseq` after the
|
|
61
|
+
* leading `{`, leaving the trailing newline untouched.
|
|
62
|
+
*/
|
|
63
|
+
write(chunk) {
|
|
64
|
+
const seq = ++this.counter;
|
|
65
|
+
const stamped = stampSeq(chunk, seq);
|
|
66
|
+
this.ringPush(seq, stamped);
|
|
67
|
+
this.fanout(stamped);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
/** Dormant in M2: `write` never returns `false`, so the writer never awaits a
|
|
71
|
+
* drain. Kept faithful to the `ByteSink` contract (and woken by
|
|
72
|
+
* {@link signalDrain}) in case a future path parks. */
|
|
73
|
+
once(_event, listener) {
|
|
74
|
+
this.drainWaiters.push(listener);
|
|
75
|
+
}
|
|
76
|
+
/** Release any (dormant) parked writer — e.g. a socket became writable. */
|
|
77
|
+
signalDrain() {
|
|
78
|
+
const waiters = this.drainWaiters;
|
|
79
|
+
this.drainWaiters = [];
|
|
80
|
+
for (const fn of waiters)
|
|
81
|
+
fn();
|
|
82
|
+
}
|
|
83
|
+
// -- ring -------------------------------------------------------------------
|
|
84
|
+
ringPush(seq, line) {
|
|
85
|
+
if (this.count < this.capacity) {
|
|
86
|
+
this.buf[(this.start + this.count) % this.capacity] = { seq, line };
|
|
87
|
+
this.count++;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Full: overwrite the oldest slot and advance the floor. O(1).
|
|
91
|
+
this.buf[this.start] = { seq, line };
|
|
92
|
+
this.start = (this.start + 1) % this.capacity;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** The highest `rseq` assigned (0 ⇒ nothing written yet). */
|
|
96
|
+
get headSeq() {
|
|
97
|
+
return this.counter;
|
|
98
|
+
}
|
|
99
|
+
/** The oldest `rseq` still replayable (0 ⇒ ring empty). */
|
|
100
|
+
get floorSeq() {
|
|
101
|
+
return this.count === 0 ? 0 : this.buf[this.start].seq;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Frames a client at `lastSeq` has missed. `reset` ⇒ its cursor is below the
|
|
105
|
+
* ring floor (unrecoverable) — the caller cold-reloads it with the whole ring.
|
|
106
|
+
* Mirrors goose `subscribe`: `last_id > 0 && floor > 0 && last_id < floor`
|
|
107
|
+
* ⇒ too far behind.
|
|
108
|
+
*/
|
|
109
|
+
replaySince(lastSeq) {
|
|
110
|
+
const floor = this.floorSeq;
|
|
111
|
+
const reset = lastSeq > 0 && floor > 0 && lastSeq < floor;
|
|
112
|
+
const since = reset ? 0 : lastSeq;
|
|
113
|
+
const frames = [];
|
|
114
|
+
// Ring order is strictly ascending in `seq`; collect the suffix `seq > since`.
|
|
115
|
+
for (let i = 0; i < this.count; i++) {
|
|
116
|
+
const slot = this.buf[(this.start + i) % this.capacity];
|
|
117
|
+
if (slot.seq > since)
|
|
118
|
+
frames.push(slot.line);
|
|
119
|
+
}
|
|
120
|
+
return { reset, frames };
|
|
121
|
+
}
|
|
122
|
+
// -- fan-out ----------------------------------------------------------------
|
|
123
|
+
/** Add a socket to the live fan-out set. Caller (SessionHost) has already sent
|
|
124
|
+
* the welcome + replay synchronously, so the first live frame this socket
|
|
125
|
+
* sees is strictly newer than everything replayed (disjoint, in order). */
|
|
126
|
+
addSocket(socket) {
|
|
127
|
+
this.sockets.add(socket);
|
|
128
|
+
}
|
|
129
|
+
removeSocket(socket) {
|
|
130
|
+
this.sockets.delete(socket);
|
|
131
|
+
}
|
|
132
|
+
get socketCount() {
|
|
133
|
+
return this.sockets.size;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Send one stamped frame to a single socket, guarded: a dead socket
|
|
137
|
+
* (`send===0`) or a wedged one (buffer past the hard cap) is dropped + closed,
|
|
138
|
+
* never throwing, never blocking the others. Returns whether the socket stays
|
|
139
|
+
* attached. Reused by {@link SessionHost} for the synchronous replay path so
|
|
140
|
+
* replay + live share one robust send.
|
|
141
|
+
*/
|
|
142
|
+
sendTo(socket, line) {
|
|
143
|
+
let sent;
|
|
144
|
+
try {
|
|
145
|
+
sent = socket.send(line);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
this.sockets.delete(socket);
|
|
149
|
+
return false; // socket threw mid-send (closing) — isolate it
|
|
150
|
+
}
|
|
151
|
+
if (sent === 0) {
|
|
152
|
+
this.sockets.delete(socket);
|
|
153
|
+
return false; // dropped/dead
|
|
154
|
+
}
|
|
155
|
+
const buffered = socket.bufferedAmount;
|
|
156
|
+
if (buffered !== undefined && buffered > WEDGED_SOCKET_BYTES) {
|
|
157
|
+
this.sockets.delete(socket);
|
|
158
|
+
try {
|
|
159
|
+
socket.close(1013, "relay: client too slow");
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
/* already gone */
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return true; // sent or backpressured-but-enqueued (-1) — stays attached
|
|
167
|
+
}
|
|
168
|
+
fanout(stamped) {
|
|
169
|
+
if (this.sockets.size === 0)
|
|
170
|
+
return;
|
|
171
|
+
// Snapshot so a removal during iteration (a guard-drop) is safe.
|
|
172
|
+
for (const socket of [...this.sockets])
|
|
173
|
+
this.sendTo(socket, stamped);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=replay-hub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay-hub.js","sourceRoot":"","sources":["../../src/serve/replay-hub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,QAAQ,EAAiB,MAAM,iBAAiB,CAAC;AAG1D;6EAC6E;AAC7E,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAE1C;;8EAE8E;AAC9E,MAAM,mBAAmB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAoB7C,MAAM,OAAO,SAAS;IACH,QAAQ,CAAS;IAClC;oFACgF;IAC/D,GAAG,CAA2B;IACvC,KAAK,GAAG,CAAC,CAAC;IACV,KAAK,GAAG,CAAC,CAAC;IAEV,OAAO,GAAG,CAAC,CAAC,CAAC,yCAAyC;IAE7C,OAAO,GAAG,IAAI,GAAG,EAAgB,CAAC;IAC3C,YAAY,GAAsB,EAAE,CAAC;IAE7C,YAAY,OAAyB,EAAE;QACrC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,IAAI,qBAAqB,CAAC,CAAC;QACpE,IAAI,CAAC,GAAG,GAAG,IAAI,KAAK,CAAuB,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC5D,CAAC;IAED,8EAA8E;IAE9E;;;;;OAKG;IACH,KAAK,CAAC,KAAa;QACjB,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC;QAC3B,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC5B,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;4DAEwD;IACxD,IAAI,CAAC,MAAe,EAAE,QAAoB;QACxC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACnC,CAAC;IAED,2EAA2E;IAC3E,WAAW;QACT,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC;QAClC,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,EAAE,IAAI,OAAO;YAAE,EAAE,EAAE,CAAC;IACjC,CAAC;IAED,8EAA8E;IAEtE,QAAQ,CAAC,GAAW,EAAE,IAAY;QACxC,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;YACpE,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC;aAAM,CAAC;YACN,+DAA+D;YAC/D,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;YACrC,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;QAChD,CAAC;IACH,CAAC;IAED,6DAA6D;IAC7D,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,2DAA2D;IAC3D,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAE,CAAC,GAAG,CAAC;IAC1D,CAAC;IAED;;;;;OAKG;IACH,WAAW,CAAC,OAAe;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC5B,MAAM,KAAK,GAAG,OAAO,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,OAAO,GAAG,KAAK,CAAC;QAC1D,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;QAClC,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,+EAA+E;QAC/E,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YACpC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAE,CAAC;YACzD,IAAI,IAAI,CAAC,GAAG,GAAG,KAAK;gBAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAC3B,CAAC;IAED,8EAA8E;IAE9E;;gFAE4E;IAC5E,SAAS,CAAC,MAAoB;QAC5B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAED,YAAY,CAAC,MAAoB;QAC/B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;IAED,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,MAAoB,EAAE,IAAY;QACvC,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC5B,OAAO,KAAK,CAAC,CAAC,+CAA+C;QAC/D,CAAC;QACD,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;YACf,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC5B,OAAO,KAAK,CAAC,CAAC,eAAe;QAC/B,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,CAAC,cAAc,CAAC;QACvC,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,GAAG,mBAAmB,EAAE,CAAC;YAC7D,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC5B,IAAI,CAAC;gBACH,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,wBAAwB,CAAC,CAAC;YAC/C,CAAC;YAAC,MAAM,CAAC;gBACP,kBAAkB;YACpB,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,IAAI,CAAC,CAAC,2DAA2D;IAC1E,CAAC;IAEO,MAAM,CAAC,OAAe;QAC5B,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO;QACpC,iEAAiE;QACjE,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;YAAE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvE,CAAC;CACF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface SelfSignedCert {
|
|
2
|
+
/** PEM certificate. */
|
|
3
|
+
cert: string;
|
|
4
|
+
/** PEM private key. */
|
|
5
|
+
key: string;
|
|
6
|
+
/** SHA-256 fingerprint of the DER cert: uppercase colon-separated hex (goose's
|
|
7
|
+
* format — exactly what a client pins). */
|
|
8
|
+
fingerprint: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Generate a self-signed EC (P-256, ECDSA-SHA256) cert valid for `hosts` (always
|
|
12
|
+
* plus `localhost` + `127.0.0.1`). `now` is injected so the validity window is
|
|
13
|
+
* deterministic in tests. Async — WebCrypto under the hood (proven to survive
|
|
14
|
+
* `bun --compile` in the Stage 0 spike).
|
|
15
|
+
*/
|
|
16
|
+
export declare function generateSelfSignedCert(hosts: string[], now?: number, validityDays?: number): Promise<SelfSignedCert>;
|
|
17
|
+
/** SHA-256 fingerprint of a PEM cert (over its DER bytes), uppercase colon hex —
|
|
18
|
+
* the value printed to stderr + embedded in the QR + pinned by clients. */
|
|
19
|
+
export declare function fingerprintOf(certPem: string): string;
|
|
20
|
+
//# sourceMappingURL=tls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tls.d.ts","sourceRoot":"","sources":["../../src/serve/tls.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC7B,uBAAuB;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,uBAAuB;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ;gDAC4C;IAC5C,WAAW,EAAE,MAAM,CAAC;CACrB;AAMD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,GAAE,MAAmB,EAAE,YAAY,GAAE,MAA8B,GAAG,OAAO,CAAC,cAAc,CAAC,CAa7J;AAED;4EAC4E;AAC5E,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAErD"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (v21 M4 / docs/6.5 §3) Self-signed TLS for a non-loopback `chances serve`,
|
|
3
|
+
* mirroring goose's model (`crates/goose-server/src/tls.rs`). On a LAN bind we
|
|
4
|
+
* generate a self-signed cert (SAN = the bound host + `localhost` + `127.0.0.1`),
|
|
5
|
+
* print its SHA-256 fingerprint, and clients PIN it (TOFU — defeats LAN MITM even
|
|
6
|
+
* with a self-signed cert). The CLI CACHES the cert (in the vault) so the
|
|
7
|
+
* fingerprint is stable across restarts — a client pairs once.
|
|
8
|
+
*
|
|
9
|
+
* This module is PURE cert generation + fingerprinting; caching/storage is the
|
|
10
|
+
* CLI's job (the relay stays storage-free). The fingerprint is SHA-256 of the DER
|
|
11
|
+
* cert, derived here via node:crypto's `X509Certificate.fingerprint256` — NOT
|
|
12
|
+
* selfsigned's own `.fingerprint`, which is SHA-1 (the Stage 0 spike caught this).
|
|
13
|
+
*/
|
|
14
|
+
import { X509Certificate } from "node:crypto";
|
|
15
|
+
import { generate } from "selfsigned";
|
|
16
|
+
/** Default cert validity. 825 days = the CA/Browser-forum max for leaf certs;
|
|
17
|
+
* ample for a long-lived local pairing, re-mintable any time. */
|
|
18
|
+
const DEFAULT_VALIDITY_DAYS = 825;
|
|
19
|
+
/**
|
|
20
|
+
* Generate a self-signed EC (P-256, ECDSA-SHA256) cert valid for `hosts` (always
|
|
21
|
+
* plus `localhost` + `127.0.0.1`). `now` is injected so the validity window is
|
|
22
|
+
* deterministic in tests. Async — WebCrypto under the hood (proven to survive
|
|
23
|
+
* `bun --compile` in the Stage 0 spike).
|
|
24
|
+
*/
|
|
25
|
+
export async function generateSelfSignedCert(hosts, now = Date.now(), validityDays = DEFAULT_VALIDITY_DAYS) {
|
|
26
|
+
const pems = await generate([{ name: "commonName", value: "chances serve" }], {
|
|
27
|
+
keyType: "ec",
|
|
28
|
+
curve: "P-256",
|
|
29
|
+
algorithm: "sha256",
|
|
30
|
+
notBeforeDate: new Date(now),
|
|
31
|
+
notAfterDate: new Date(now + validityDays * 24 * 3600 * 1000),
|
|
32
|
+
extensions: [
|
|
33
|
+
{ name: "basicConstraints", cA: false, critical: true },
|
|
34
|
+
{ name: "subjectAltName", altNames: buildAltNames(hosts) },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
return { cert: pems.cert, key: pems.private, fingerprint: fingerprintOf(pems.cert) };
|
|
38
|
+
}
|
|
39
|
+
/** SHA-256 fingerprint of a PEM cert (over its DER bytes), uppercase colon hex —
|
|
40
|
+
* the value printed to stderr + embedded in the QR + pinned by clients. */
|
|
41
|
+
export function fingerprintOf(certPem) {
|
|
42
|
+
return new X509Certificate(certPem).fingerprint256;
|
|
43
|
+
}
|
|
44
|
+
/** SAN list: `localhost` + `127.0.0.1` are always present (the loopback aliases),
|
|
45
|
+
* plus each requested host classified as DNS (type 2) or IP (type 7). Deduped. */
|
|
46
|
+
function buildAltNames(hosts) {
|
|
47
|
+
const dns = new Set(["localhost"]);
|
|
48
|
+
const ips = new Set(["127.0.0.1"]);
|
|
49
|
+
for (const h of hosts) {
|
|
50
|
+
if (h === "0.0.0.0" || h === "::")
|
|
51
|
+
continue; // wildcard binds are not a SAN
|
|
52
|
+
if (isIpLiteral(h))
|
|
53
|
+
ips.add(h);
|
|
54
|
+
else
|
|
55
|
+
dns.add(h);
|
|
56
|
+
}
|
|
57
|
+
return [...[...dns].map((value) => ({ type: 2, value })), ...[...ips].map((ip) => ({ type: 7, ip }))];
|
|
58
|
+
}
|
|
59
|
+
/** Crude IPv4/IPv6 literal check (enough to route a host into the right SAN
|
|
60
|
+
* slot; a hostname with letters is DNS, a dotted-quad or colon form is an IP). */
|
|
61
|
+
function isIpLiteral(h) {
|
|
62
|
+
return /^\d{1,3}(\.\d{1,3}){3}$/.test(h) || h.includes(":");
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=tls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tls.js","sourceRoot":"","sources":["../../src/serve/tls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAYtC;kEACkE;AAClE,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAElC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,KAAe,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE,EAAE,eAAuB,qBAAqB;IAClI,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,EAAE;QAC5E,OAAO,EAAE,IAAI;QACb,KAAK,EAAE,OAAO;QACd,SAAS,EAAE,QAAQ;QACnB,aAAa,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC;QAC5B,YAAY,EAAE,IAAI,IAAI,CAAC,GAAG,GAAG,YAAY,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;QAC7D,UAAU,EAAE;YACV,EAAE,IAAI,EAAE,kBAAkB,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE;YACvD,EAAE,IAAI,EAAE,gBAAgB,EAAE,QAAQ,EAAE,aAAa,CAAC,KAAK,CAAC,EAAE;SAC3D;KACF,CAAC,CAAC;IACH,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AACvF,CAAC;AAED;4EAC4E;AAC5E,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,OAAO,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC;AACrD,CAAC;AAED;mFACmF;AACnF,SAAS,aAAa,CAAC,KAAe;IACpC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAS,CAAC,WAAW,CAAC,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAS,CAAC,WAAW,CAAC,CAAC,CAAC;IAC3C,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI;YAAE,SAAS,CAAC,+BAA+B;QAC5E,IAAI,WAAW,CAAC,CAAC,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;;YAC1B,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAU,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAU,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;AAC1H,CAAC;AAED;mFACmF;AACnF,SAAS,WAAW,CAAC,CAAS;IAC5B,OAAO,yBAAyB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC9D,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (v17 M1 / v20 M3) Relay transport primitives shared by the ACP control
|
|
3
|
+
* channel. M1's `RpcServer`-per-socket binding (`RpcSocketSession` + `WsByteSink`)
|
|
4
|
+
* was retired in the M3 hard cutover: the relay now runs ONE persistent
|
|
5
|
+
* `AcpSessionHost` whose `AcpEngineDriver` writes through the `ReplayHub`, so the
|
|
6
|
+
* only pieces left here are the transport-shape primitives the relay +
|
|
7
|
+
* `AcpSessionHost` reuse:
|
|
8
|
+
* - {@link ServerSocket}: the minimal server-socket surface the fan-out sends through.
|
|
9
|
+
* - {@link MessageQueue}: a push-driven `AsyncIterable<string>` feeding the driver's input.
|
|
10
|
+
* - {@link perConnectionHost}: makes per-connection shutdown a no-op so the
|
|
11
|
+
* shared runtime outlives individual sockets.
|
|
12
|
+
*
|
|
13
|
+
* Socket-free by construction (tests inject a recording fake `ServerSocket`).
|
|
14
|
+
*/
|
|
15
|
+
import type { EngineHost } from "../rpc/index.js";
|
|
16
|
+
/**
|
|
17
|
+
* The minimal server-socket surface the {@link ReplayHub} fan-out sends through.
|
|
18
|
+
* A `ws` WebSocket (adapted by the relay's `nodeServerSocket`) satisfies it; tests
|
|
19
|
+
* inject a recording fake. `send` returns a tri-state: `>0` bytes written, `-1`
|
|
20
|
+
* enqueued under backpressure, `0` dropped (connection gone).
|
|
21
|
+
*/
|
|
22
|
+
export interface ServerSocket {
|
|
23
|
+
send(data: string): number;
|
|
24
|
+
close(code?: number, reason?: string): void;
|
|
25
|
+
/** Bytes queued in the socket's own send buffer, if the transport exposes it
|
|
26
|
+
* (`ws.bufferedAmount`). The {@link ReplayHub} fan-out reads it to drop a
|
|
27
|
+
* WEDGED socket past a hard cap so a single dead remote can't grow relay
|
|
28
|
+
* memory unbounded — the dropped client reconnects + replays from the ring.
|
|
29
|
+
* Absent on test fakes (the cap is then simply never tripped). */
|
|
30
|
+
readonly bufferedAmount?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A push-driven `AsyncIterable<string>` feeding `AcpEngineDriver.run`. WebSocket
|
|
34
|
+
* preserves message boundaries, so each inbound message is one (or a few
|
|
35
|
+
* LF-batched) ACP JSON-RPC line(s); {@link AcpSessionHost.onMessage} splits and
|
|
36
|
+
* {@link push}es each. Single-consumer (the driver iterates once); items pushed
|
|
37
|
+
* before the consumer awaits are buffered in order.
|
|
38
|
+
*/
|
|
39
|
+
export declare class MessageQueue implements AsyncIterable<string> {
|
|
40
|
+
private readonly buffer;
|
|
41
|
+
private waiting;
|
|
42
|
+
private ended;
|
|
43
|
+
push(line: string): void;
|
|
44
|
+
/** No more input (process shutdown) — drains the buffer, then ends the iterator. */
|
|
45
|
+
end(): void;
|
|
46
|
+
[Symbol.asyncIterator](): AsyncIterator<string>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Wrap an `EngineHost` so per-connection / per-session shutdown does NOT tear
|
|
50
|
+
* down the shared runtime. The CLI's `makeEngineHost(ctx).dispose()` runs the
|
|
51
|
+
* whole-process `disposeRuntime(ctx)` — correct for `chances rpc` (one stdio
|
|
52
|
+
* client = process lifetime) but wrong for a relay that holds a PERSISTENT
|
|
53
|
+
* session outliving individual sockets. The real teardown happens once when
|
|
54
|
+
* `chances serve` exits (`main()`'s `finally`, idempotent). `build`/`listModels`
|
|
55
|
+
* delegate unchanged; only `dispose` becomes a no-op.
|
|
56
|
+
*
|
|
57
|
+
* (v23 M5) The optional read-only workspace queries are FORWARDED when present —
|
|
58
|
+
* the relay reads them off its host to advertise `workspaceQueries` and answer
|
|
59
|
+
* `list_files`/`read_file`/`git_*`. Dropping them here (as the original wrapper
|
|
60
|
+
* did) silently disabled the 3-pane IDE over the relay even though the CLI host
|
|
61
|
+
* supported it; the desktop's real-socket test catches that regression.
|
|
62
|
+
*/
|
|
63
|
+
export declare function perConnectionHost(host: EngineHost): EngineHost;
|
|
64
|
+
//# sourceMappingURL=ws-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-transport.d.ts","sourceRoot":"","sources":["../../src/serve/ws-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C;;;;uEAImE;IACnE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;GAMG;AACH,qBAAa,YAAa,YAAW,aAAa,CAAC,MAAM,CAAC;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,OAAO,CAAsD;IACrE,OAAO,CAAC,KAAK,CAAS;IAEtB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAWxB,oFAAoF;IACpF,GAAG,IAAI,IAAI;IAUJ,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,MAAM,CAAC;CAcvD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAU9D"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* (v17 M1 / v20 M3) Relay transport primitives shared by the ACP control
|
|
3
|
+
* channel. M1's `RpcServer`-per-socket binding (`RpcSocketSession` + `WsByteSink`)
|
|
4
|
+
* was retired in the M3 hard cutover: the relay now runs ONE persistent
|
|
5
|
+
* `AcpSessionHost` whose `AcpEngineDriver` writes through the `ReplayHub`, so the
|
|
6
|
+
* only pieces left here are the transport-shape primitives the relay +
|
|
7
|
+
* `AcpSessionHost` reuse:
|
|
8
|
+
* - {@link ServerSocket}: the minimal server-socket surface the fan-out sends through.
|
|
9
|
+
* - {@link MessageQueue}: a push-driven `AsyncIterable<string>` feeding the driver's input.
|
|
10
|
+
* - {@link perConnectionHost}: makes per-connection shutdown a no-op so the
|
|
11
|
+
* shared runtime outlives individual sockets.
|
|
12
|
+
*
|
|
13
|
+
* Socket-free by construction (tests inject a recording fake `ServerSocket`).
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* A push-driven `AsyncIterable<string>` feeding `AcpEngineDriver.run`. WebSocket
|
|
17
|
+
* preserves message boundaries, so each inbound message is one (or a few
|
|
18
|
+
* LF-batched) ACP JSON-RPC line(s); {@link AcpSessionHost.onMessage} splits and
|
|
19
|
+
* {@link push}es each. Single-consumer (the driver iterates once); items pushed
|
|
20
|
+
* before the consumer awaits are buffered in order.
|
|
21
|
+
*/
|
|
22
|
+
export class MessageQueue {
|
|
23
|
+
buffer = [];
|
|
24
|
+
waiting = null;
|
|
25
|
+
ended = false;
|
|
26
|
+
push(line) {
|
|
27
|
+
if (this.ended)
|
|
28
|
+
return;
|
|
29
|
+
if (this.waiting) {
|
|
30
|
+
const resolve = this.waiting;
|
|
31
|
+
this.waiting = null;
|
|
32
|
+
resolve({ value: line, done: false });
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.buffer.push(line);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** No more input (process shutdown) — drains the buffer, then ends the iterator. */
|
|
39
|
+
end() {
|
|
40
|
+
if (this.ended)
|
|
41
|
+
return;
|
|
42
|
+
this.ended = true;
|
|
43
|
+
if (this.waiting) {
|
|
44
|
+
const resolve = this.waiting;
|
|
45
|
+
this.waiting = null;
|
|
46
|
+
resolve({ value: undefined, done: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async *[Symbol.asyncIterator]() {
|
|
50
|
+
while (true) {
|
|
51
|
+
if (this.buffer.length > 0) {
|
|
52
|
+
yield this.buffer.shift();
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (this.ended)
|
|
56
|
+
return;
|
|
57
|
+
const next = await new Promise((resolve) => {
|
|
58
|
+
this.waiting = resolve;
|
|
59
|
+
});
|
|
60
|
+
if (next.done)
|
|
61
|
+
return;
|
|
62
|
+
yield next.value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Wrap an `EngineHost` so per-connection / per-session shutdown does NOT tear
|
|
68
|
+
* down the shared runtime. The CLI's `makeEngineHost(ctx).dispose()` runs the
|
|
69
|
+
* whole-process `disposeRuntime(ctx)` — correct for `chances rpc` (one stdio
|
|
70
|
+
* client = process lifetime) but wrong for a relay that holds a PERSISTENT
|
|
71
|
+
* session outliving individual sockets. The real teardown happens once when
|
|
72
|
+
* `chances serve` exits (`main()`'s `finally`, idempotent). `build`/`listModels`
|
|
73
|
+
* delegate unchanged; only `dispose` becomes a no-op.
|
|
74
|
+
*
|
|
75
|
+
* (v23 M5) The optional read-only workspace queries are FORWARDED when present —
|
|
76
|
+
* the relay reads them off its host to advertise `workspaceQueries` and answer
|
|
77
|
+
* `list_files`/`read_file`/`git_*`. Dropping them here (as the original wrapper
|
|
78
|
+
* did) silently disabled the 3-pane IDE over the relay even though the CLI host
|
|
79
|
+
* supported it; the desktop's real-socket test catches that regression.
|
|
80
|
+
*/
|
|
81
|
+
export function perConnectionHost(host) {
|
|
82
|
+
return {
|
|
83
|
+
build: (resolver) => host.build(resolver),
|
|
84
|
+
listModels: () => host.listModels(),
|
|
85
|
+
dispose: async () => { },
|
|
86
|
+
...(host.queryListFiles ? { queryListFiles: host.queryListFiles.bind(host) } : {}),
|
|
87
|
+
...(host.queryReadFile ? { queryReadFile: host.queryReadFile.bind(host) } : {}),
|
|
88
|
+
...(host.queryGitStatus ? { queryGitStatus: host.queryGitStatus.bind(host) } : {}),
|
|
89
|
+
...(host.queryGitDiff ? { queryGitDiff: host.queryGitDiff.bind(host) } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=ws-transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-transport.js","sourceRoot":"","sources":["../../src/serve/ws-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAqBH;;;;;;GAMG;AACH,MAAM,OAAO,YAAY;IACN,MAAM,GAAa,EAAE,CAAC;IAC/B,OAAO,GAAiD,IAAI,CAAC;IAC7D,KAAK,GAAG,KAAK,CAAC;IAEtB,IAAI,CAAC,IAAY;QACf,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;YAC7B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACxC,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,oFAAoF;IACpF,GAAG;QACD,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;YAC7B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,OAAO,CAAC,EAAE,KAAK,EAAE,SAAkB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC;QAC3B,OAAO,IAAI,EAAE,CAAC;YACZ,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3B,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAG,CAAC;gBAC3B,SAAS;YACX,CAAC;YACD,IAAI,IAAI,CAAC,KAAK;gBAAE,OAAO;YACvB,MAAM,IAAI,GAAG,MAAM,IAAI,OAAO,CAAyB,CAAC,OAAO,EAAE,EAAE;gBACjE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;YACzB,CAAC,CAAC,CAAC;YACH,IAAI,IAAI,CAAC,IAAI;gBAAE,OAAO;YACtB,MAAM,IAAI,CAAC,KAAK,CAAC;QACnB,CAAC;IACH,CAAC;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAgB;IAChD,OAAO;QACL,KAAK,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;QACzC,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,EAAE;QACnC,OAAO,EAAE,KAAK,IAAI,EAAE,GAAE,CAAC;QACvB,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClF,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClF,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC7E,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chances-ai/wire",
|
|
3
|
+
"version": "24.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
"./rpc": {
|
|
7
|
+
"types": "./dist/rpc/index.d.ts",
|
|
8
|
+
"import": "./dist/rpc/index.js"
|
|
9
|
+
},
|
|
10
|
+
"./serve": {
|
|
11
|
+
"types": "./dist/serve/index.d.ts",
|
|
12
|
+
"import": "./dist/serve/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@chances-ai/engine": "24.0.0",
|
|
20
|
+
"@chances-ai/runtime": "24.0.0",
|
|
21
|
+
"@chances-ai/ui-core": "24.0.0",
|
|
22
|
+
"selfsigned": "^5.5.0",
|
|
23
|
+
"ws": "^8.18.0",
|
|
24
|
+
"zod": "^3.23.8"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@agentclientprotocol/sdk": "^0.22.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"@agentclientprotocol/sdk": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@agentclientprotocol/sdk": "^0.22.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -b",
|
|
39
|
+
"check": "tsc -b",
|
|
40
|
+
"test": "bun test --timeout 30000"
|
|
41
|
+
}
|
|
42
|
+
}
|