@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.
Files changed (93) hide show
  1. package/dist/rpc/acp/adapter.d.ts +32 -0
  2. package/dist/rpc/acp/adapter.d.ts.map +1 -0
  3. package/dist/rpc/acp/adapter.js +185 -0
  4. package/dist/rpc/acp/adapter.js.map +1 -0
  5. package/dist/rpc/acp/engine-driver.d.ts +128 -0
  6. package/dist/rpc/acp/engine-driver.d.ts.map +1 -0
  7. package/dist/rpc/acp/engine-driver.js +550 -0
  8. package/dist/rpc/acp/engine-driver.js.map +1 -0
  9. package/dist/rpc/acp/event-map.d.ts +22 -0
  10. package/dist/rpc/acp/event-map.d.ts.map +1 -0
  11. package/dist/rpc/acp/event-map.js +205 -0
  12. package/dist/rpc/acp/event-map.js.map +1 -0
  13. package/dist/rpc/acp/load-sdk.d.ts +3 -0
  14. package/dist/rpc/acp/load-sdk.d.ts.map +1 -0
  15. package/dist/rpc/acp/load-sdk.js +24 -0
  16. package/dist/rpc/acp/load-sdk.js.map +1 -0
  17. package/dist/rpc/acp/workspace-query.d.ts +41 -0
  18. package/dist/rpc/acp/workspace-query.d.ts.map +1 -0
  19. package/dist/rpc/acp/workspace-query.js +89 -0
  20. package/dist/rpc/acp/workspace-query.js.map +1 -0
  21. package/dist/rpc/driver.d.ts +42 -0
  22. package/dist/rpc/driver.d.ts.map +1 -0
  23. package/dist/rpc/driver.js +7 -0
  24. package/dist/rpc/driver.js.map +1 -0
  25. package/dist/rpc/event-map.d.ts +8 -0
  26. package/dist/rpc/event-map.d.ts.map +1 -0
  27. package/dist/rpc/event-map.js +91 -0
  28. package/dist/rpc/event-map.js.map +1 -0
  29. package/dist/rpc/index.d.ts +13 -0
  30. package/dist/rpc/index.d.ts.map +1 -0
  31. package/dist/rpc/index.js +18 -0
  32. package/dist/rpc/index.js.map +1 -0
  33. package/dist/rpc/lines.d.ts +2 -0
  34. package/dist/rpc/lines.d.ts.map +1 -0
  35. package/dist/rpc/lines.js +24 -0
  36. package/dist/rpc/lines.js.map +1 -0
  37. package/dist/rpc/protocol.d.ts +315 -0
  38. package/dist/rpc/protocol.d.ts.map +1 -0
  39. package/dist/rpc/protocol.js +70 -0
  40. package/dist/rpc/protocol.js.map +1 -0
  41. package/dist/rpc/rpc-server.d.ts +56 -0
  42. package/dist/rpc/rpc-server.d.ts.map +1 -0
  43. package/dist/rpc/rpc-server.js +305 -0
  44. package/dist/rpc/rpc-server.js.map +1 -0
  45. package/dist/rpc/stdout-guard.d.ts +5 -0
  46. package/dist/rpc/stdout-guard.d.ts.map +1 -0
  47. package/dist/rpc/stdout-guard.js +31 -0
  48. package/dist/rpc/stdout-guard.js.map +1 -0
  49. package/dist/rpc/writer.d.ts +34 -0
  50. package/dist/rpc/writer.d.ts.map +1 -0
  51. package/dist/rpc/writer.js +85 -0
  52. package/dist/rpc/writer.js.map +1 -0
  53. package/dist/serve/acp-session-host.d.ts +120 -0
  54. package/dist/serve/acp-session-host.d.ts.map +1 -0
  55. package/dist/serve/acp-session-host.js +276 -0
  56. package/dist/serve/acp-session-host.js.map +1 -0
  57. package/dist/serve/auth.d.ts +21 -0
  58. package/dist/serve/auth.d.ts.map +1 -0
  59. package/dist/serve/auth.js +58 -0
  60. package/dist/serve/auth.js.map +1 -0
  61. package/dist/serve/highlight.d.ts +25 -0
  62. package/dist/serve/highlight.d.ts.map +1 -0
  63. package/dist/serve/highlight.js +28 -0
  64. package/dist/serve/highlight.js.map +1 -0
  65. package/dist/serve/index.d.ts +14 -0
  66. package/dist/serve/index.d.ts.map +1 -0
  67. package/dist/serve/index.js +23 -0
  68. package/dist/serve/index.js.map +1 -0
  69. package/dist/serve/pairing.d.ts +25 -0
  70. package/dist/serve/pairing.d.ts.map +1 -0
  71. package/dist/serve/pairing.js +10 -0
  72. package/dist/serve/pairing.js.map +1 -0
  73. package/dist/serve/relay-frames.d.ts +29 -0
  74. package/dist/serve/relay-frames.d.ts.map +1 -0
  75. package/dist/serve/relay-frames.js +54 -0
  76. package/dist/serve/relay-frames.js.map +1 -0
  77. package/dist/serve/relay.d.ts +146 -0
  78. package/dist/serve/relay.d.ts.map +1 -0
  79. package/dist/serve/relay.js +475 -0
  80. package/dist/serve/relay.js.map +1 -0
  81. package/dist/serve/replay-hub.d.ts +102 -0
  82. package/dist/serve/replay-hub.d.ts.map +1 -0
  83. package/dist/serve/replay-hub.js +176 -0
  84. package/dist/serve/replay-hub.js.map +1 -0
  85. package/dist/serve/tls.d.ts +20 -0
  86. package/dist/serve/tls.d.ts.map +1 -0
  87. package/dist/serve/tls.js +64 -0
  88. package/dist/serve/tls.js.map +1 -0
  89. package/dist/serve/ws-transport.d.ts +64 -0
  90. package/dist/serve/ws-transport.d.ts.map +1 -0
  91. package/dist/serve/ws-transport.js +92 -0
  92. package/dist/serve/ws-transport.js.map +1 -0
  93. package/package.json +42 -0
@@ -0,0 +1,305 @@
1
+ // The bespoke RPC server: a thin orchestrator over the `EngineHost` seam
2
+ // (codex C1). It subscribes the engine bus → outbound frames, dispatches
3
+ // inbound commands via a method table (C8), owns the permission round-trip
4
+ // (deny-on-abort/close + dedup — C7, Round-1 M2), and runs the shutdown order
5
+ // (cancel → deny → dispose → flush — Round-1 S4). No transport I/O lives here:
6
+ // `run()` consumes an async-iterable of input lines and writes through the
7
+ // injected `BoundedNdjsonWriter`, so the whole thing is unit-testable
8
+ // in-process against a MockAdapter-backed engine.
9
+ import { CancellationTokenSource, createId } from "@chances-ai/runtime";
10
+ import { mapEvent } from "./event-map.js";
11
+ import { CommandSchema, PROTOCOL_NAME, PROTOCOL_VERSION, } from "./protocol.js";
12
+ import { BoundedNdjsonWriter } from "./writer.js";
13
+ const KNOWN_TYPES = new Set([
14
+ "prompt",
15
+ "abort",
16
+ "set_model",
17
+ "get_state",
18
+ "get_models",
19
+ "permission_response",
20
+ "set_options",
21
+ "ping",
22
+ ]);
23
+ const MAX_RESOLVED_IDS = 1024;
24
+ /** Bound on awaiting an in-flight turn during shutdown (Round-2 M1). Matches
25
+ * the CLI's other 2 s teardown deadlines. */
26
+ const SHUTDOWN_TURN_DEADLINE_MS = 2_000;
27
+ export class RpcServer {
28
+ host;
29
+ writer;
30
+ agent;
31
+ autoApprove;
32
+ toolChunks;
33
+ logSink;
34
+ built = null;
35
+ unsub = null;
36
+ activeTurn = null;
37
+ /** The in-flight `handlePrompt` promise (fire-and-forget from the read
38
+ * loop). Tracked so shutdown can await its terminal `result` before
39
+ * closing the writer (Round-2 M1). */
40
+ activePrompt = null;
41
+ pending = new Map();
42
+ resolvedIds = [];
43
+ resolvedSet = new Set();
44
+ shuttingDown = false;
45
+ constructor(opts) {
46
+ this.host = opts.host;
47
+ this.writer = opts.writer;
48
+ this.agent = opts.agent;
49
+ this.autoApprove = opts.autoApprove ?? false;
50
+ this.toolChunks = opts.toolChunks ?? true;
51
+ this.logSink = opts.logSink ?? ((line) => process.stderr.write(line));
52
+ }
53
+ /** Drive the server until `lines` ends, then shut down. Returns an exit code. */
54
+ async run(lines) {
55
+ const built = this.host.build(this.makeResolver());
56
+ this.built = built;
57
+ // Subscribe BEFORE ready so no frame is missed. `log` is routed to stderr
58
+ // (stdout purity); everything else maps to a wire frame or is suppressed.
59
+ this.unsub = built.bus.onAny((e) => {
60
+ if (e.type === "log") {
61
+ this.logSink(`[${e.level}] ${e.message}\n`);
62
+ return;
63
+ }
64
+ const frame = mapEvent(e, { toolChunks: this.toolChunks });
65
+ if (!frame)
66
+ return;
67
+ if (frame.type === "turn_start")
68
+ frame.commandId = this.activeTurn?.commandId;
69
+ this.writer.enqueue(frame);
70
+ });
71
+ this.writer.enqueue({
72
+ type: "ready",
73
+ protocol: PROTOCOL_NAME,
74
+ version: PROTOCOL_VERSION,
75
+ agent: this.agent,
76
+ sessionId: built.sessionId,
77
+ models: this.host.listModels(),
78
+ capabilities: { permissionPrompt: true, toolChunks: this.toolChunks, steering: false },
79
+ });
80
+ try {
81
+ for await (const line of lines) {
82
+ const trimmed = line.trim();
83
+ if (trimmed === "")
84
+ continue;
85
+ await this.handleLine(trimmed);
86
+ }
87
+ }
88
+ finally {
89
+ await this.shutdown();
90
+ }
91
+ return 0;
92
+ }
93
+ /** Idempotent shutdown. Order (Round-1 S4): cancel the turn → deny pending
94
+ * permissions (so a turn blocked in the resolver unwinds — M2) → dispose
95
+ * (disposers may emit their LAST bus frames, still subscribed) → flush →
96
+ * close. */
97
+ async shutdown() {
98
+ if (this.shuttingDown)
99
+ return;
100
+ this.shuttingDown = true;
101
+ this.activeTurn?.cts.cancel();
102
+ this.denyAllPending();
103
+ // (Round-2 M1) Await the in-flight prompt so its terminal `result` frame
104
+ // is enqueued before we flush + close — otherwise EOF/SIGINT could ack a
105
+ // prompt and then drop its completion. Cancellation above makes runTurn
106
+ // settle promptly; the bounded race guards a turn that ignores the token.
107
+ if (this.activePrompt) {
108
+ await Promise.race([this.activePrompt, delay(SHUTDOWN_TURN_DEADLINE_MS)]);
109
+ }
110
+ try {
111
+ await this.host.dispose();
112
+ }
113
+ catch {
114
+ // dispose is best-effort; never let it block the flush.
115
+ }
116
+ await this.writer.flush();
117
+ this.unsub?.();
118
+ this.writer.close();
119
+ }
120
+ // -- command handling ------------------------------------------------------
121
+ async handleLine(line) {
122
+ let json;
123
+ try {
124
+ json = JSON.parse(line);
125
+ }
126
+ catch {
127
+ this.respondError(null, "parse", { code: "PARSE_ERROR", message: "invalid JSON line" });
128
+ return;
129
+ }
130
+ const parsed = CommandSchema.safeParse(json);
131
+ if (!parsed.success) {
132
+ const id = idOf(json);
133
+ const type = json?.type;
134
+ const unknown = typeof type !== "string" || !KNOWN_TYPES.has(type);
135
+ this.respondError(id, "parse", {
136
+ code: unknown ? "UNKNOWN_COMMAND" : "BAD_REQUEST",
137
+ message: parsed.error.issues[0]?.message ?? "invalid command",
138
+ });
139
+ return;
140
+ }
141
+ const cmd = parsed.data;
142
+ // `prompt` is long-running — fire-and-forget so the loop keeps reading
143
+ // `abort` / `permission_response` mid-turn. It emits its own `result`.
144
+ if (cmd.type === "prompt") {
145
+ // Fire-and-forget so the loop keeps reading abort/permission mid-turn,
146
+ // but track the promise so shutdown can await the terminal `result`
147
+ // (Round-2 M1). handlePrompt never rejects (it catches internally).
148
+ this.activePrompt = this.handlePrompt(cmd).finally(() => {
149
+ this.activePrompt = null;
150
+ });
151
+ return;
152
+ }
153
+ await this.dispatch(cmd);
154
+ }
155
+ async dispatch(cmd) {
156
+ switch (cmd.type) {
157
+ case "abort":
158
+ // Deny pending permissions FIRST so a turn parked in the resolver
159
+ // unwinds, then cancel the token (Round-1 M2).
160
+ this.denyAllPending();
161
+ this.activeTurn?.cts.cancel();
162
+ this.respondOk(cmd.id, "abort");
163
+ return;
164
+ case "set_model":
165
+ if (this.activeTurn) {
166
+ this.respondError(cmd.id ?? null, "set_model", {
167
+ code: "AGENT_BUSY",
168
+ message: "cannot switch model while a turn is running",
169
+ });
170
+ return;
171
+ }
172
+ this.built?.selection.set({ provider: cmd.provider, model: cmd.model });
173
+ this.respondOk(cmd.id, "set_model", this.built?.selection.get() ?? {});
174
+ return;
175
+ case "get_state":
176
+ this.respondOk(cmd.id, "get_state", {
177
+ model: this.built?.selection.get() ?? {},
178
+ busy: this.activeTurn !== null,
179
+ sessionId: this.built?.sessionId ?? "",
180
+ toolChunks: this.toolChunks,
181
+ });
182
+ return;
183
+ case "get_models":
184
+ this.respondOk(cmd.id, "get_models", { models: this.host.listModels() });
185
+ return;
186
+ case "permission_response":
187
+ this.resolvePermission(cmd.id, cmd.allow, cmd.remember);
188
+ return; // no response frame — it's a reply, not a request
189
+ case "set_options":
190
+ if (cmd.toolChunks !== undefined)
191
+ this.toolChunks = cmd.toolChunks;
192
+ this.respondOk(cmd.id, "set_options", { toolChunks: this.toolChunks });
193
+ return;
194
+ case "ping":
195
+ this.respondOk(cmd.id, "ping");
196
+ return;
197
+ }
198
+ }
199
+ async handlePrompt(cmd) {
200
+ if (this.activeTurn) {
201
+ this.respondError(cmd.id ?? null, "prompt", {
202
+ code: "AGENT_BUSY",
203
+ message: "a turn is already running",
204
+ });
205
+ return;
206
+ }
207
+ const cts = new CancellationTokenSource();
208
+ this.activeTurn = { cts, commandId: cmd.id };
209
+ this.respondOk(cmd.id, "prompt"); // ack now; completion is the `result` frame (C6)
210
+ let result = { text: "", inputTokens: 0, outputTokens: 0, costUsd: 0 };
211
+ let stopReason = "end";
212
+ try {
213
+ result = await this.built.runTurn(cmd.text, cts.token);
214
+ }
215
+ catch {
216
+ // Non-cancel engine errors already emitted an `error` bus frame before
217
+ // throwing; cancellation is user-initiated, not an error to re-surface.
218
+ stopReason = cts.token.isCancelled ? "cancelled" : "error";
219
+ }
220
+ finally {
221
+ this.activeTurn = null;
222
+ }
223
+ this.writer.enqueue({
224
+ type: "result",
225
+ commandId: cmd.id,
226
+ text: result.text,
227
+ usage: {
228
+ inputTokens: result.inputTokens,
229
+ outputTokens: result.outputTokens,
230
+ costUsd: result.costUsd,
231
+ },
232
+ stopReason,
233
+ });
234
+ }
235
+ // -- permission round-trip -------------------------------------------------
236
+ makeResolver() {
237
+ return (req) => {
238
+ // (5.10b) Narrow the union before reading authorization-only fields
239
+ // (`summary`/`args`/`cacheKey`). No remote question UI yet — the channel
240
+ // is ready, the host frame is a follow-up; a question declines for now.
241
+ if (req.kind === "question") {
242
+ const declined = { kind: "question", answers: {}, declined: true };
243
+ return Promise.resolve(declined);
244
+ }
245
+ if (this.autoApprove)
246
+ return Promise.resolve({ allow: true, remember: true });
247
+ const id = createId("perm");
248
+ return new Promise((resolve) => {
249
+ this.pending.set(id, resolve);
250
+ this.writer.enqueue({
251
+ type: "request_permission",
252
+ id,
253
+ callId: req.callId,
254
+ tool: req.name,
255
+ category: req.category,
256
+ summary: req.summary,
257
+ args: req.args,
258
+ cacheKey: req.cacheKey,
259
+ });
260
+ });
261
+ };
262
+ }
263
+ resolvePermission(id, allow, remember) {
264
+ if (this.resolvedSet.has(id))
265
+ return; // dedup orphan/duplicate (C7)
266
+ const resolve = this.pending.get(id);
267
+ if (!resolve)
268
+ return; // unknown id — ignore
269
+ this.pending.delete(id);
270
+ this.markResolved(id);
271
+ resolve({ allow, remember });
272
+ }
273
+ /** Resolve every outstanding permission with deny — unblocks a turn parked
274
+ * in the resolver on `abort` / shutdown (Round-1 M2). */
275
+ denyAllPending() {
276
+ for (const [id, resolve] of this.pending) {
277
+ this.markResolved(id);
278
+ resolve({ allow: false });
279
+ }
280
+ this.pending.clear();
281
+ }
282
+ markResolved(id) {
283
+ this.resolvedSet.add(id);
284
+ this.resolvedIds.push(id);
285
+ if (this.resolvedIds.length > MAX_RESOLVED_IDS) {
286
+ const evicted = this.resolvedIds.shift();
287
+ this.resolvedSet.delete(evicted);
288
+ }
289
+ }
290
+ // -- response helpers ------------------------------------------------------
291
+ respondOk(id, command, data) {
292
+ this.writer.enqueue({ type: "response", id: id ?? null, command, ok: true, data: data });
293
+ }
294
+ respondError(id, command, error) {
295
+ this.writer.enqueue({ type: "response", id, command, ok: false, error });
296
+ }
297
+ }
298
+ function idOf(json) {
299
+ const id = json?.id;
300
+ return typeof id === "string" ? id : null;
301
+ }
302
+ function delay(ms) {
303
+ return new Promise((resolve) => setTimeout(resolve, ms));
304
+ }
305
+ //# sourceMappingURL=rpc-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc-server.js","sourceRoot":"","sources":["../../src/rpc/rpc-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export interface StdoutGuard {
2
+ restore(): void;
3
+ }
4
+ export declare function installStdoutGuard(): StdoutGuard;
5
+ //# sourceMappingURL=stdout-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdout-guard.d.ts","sourceRoot":"","sources":["../../src/rpc/stdout-guard.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,WAAW;IAC1B,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,kBAAkB,IAAI,WAAW,CAuBhD"}
@@ -0,0 +1,31 @@
1
+ // stdout purity guard (codex C5 + Round-1 S2). stdout carries ONLY protocol
2
+ // frames; a single stray `console.log` from a dependency would corrupt the
3
+ // client's NDJSON parser. Redirect the stdout-writing console methods to
4
+ // stderr for the server's lifetime. `restore()` is idempotent and must run in
5
+ // a `finally` so a thrown server (or an embedding test) can't leak the
6
+ // override. The bus `log` event is routed to stderr by the server, not here.
7
+ export function installStdoutGuard() {
8
+ // Save the ORIGINAL references (not `.bind` copies) so `restore()` puts the
9
+ // exact same functions back — identity matters for embedders/tests.
10
+ const saved = {
11
+ log: console.log,
12
+ info: console.info,
13
+ debug: console.debug,
14
+ };
15
+ const toErr = (...args) => console.error(...args);
16
+ console.log = toErr;
17
+ console.info = toErr;
18
+ console.debug = toErr;
19
+ let restored = false;
20
+ return {
21
+ restore() {
22
+ if (restored)
23
+ return;
24
+ restored = true;
25
+ console.log = saved.log;
26
+ console.info = saved.info;
27
+ console.debug = saved.debug;
28
+ },
29
+ };
30
+ }
31
+ //# sourceMappingURL=stdout-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdout-guard.js","sourceRoot":"","sources":["../../src/rpc/stdout-guard.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,2EAA2E;AAC3E,yEAAyE;AACzE,8EAA8E;AAC9E,uEAAuE;AACvE,6EAA6E;AAQ7E,MAAM,UAAU,kBAAkB;IAChC,4EAA4E;IAC5E,oEAAoE;IACpE,MAAM,KAAK,GAAsE;QAC/E,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,KAAK,EAAE,OAAO,CAAC,KAAK;KACrB,CAAC;IACF,MAAM,KAAK,GAAkB,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,GAAG,KAAK,CAAC;IACpB,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC;IACrB,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;IAEtB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,OAAO;QACL,OAAO;YACL,IAAI,QAAQ;gBAAE,OAAO;YACrB,QAAQ,GAAG,IAAI,CAAC;YAChB,OAAO,CAAC,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC;YACxB,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;YAC1B,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QAC9B,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,34 @@
1
+ import type { ServerFrame } from "./protocol.js";
2
+ /** The subset of a Node Writable the writer needs. `process.stdout` satisfies
3
+ * it; tests inject an in-memory sink. */
4
+ export interface ByteSink {
5
+ write(chunk: string): boolean;
6
+ once(event: "drain", listener: () => void): void;
7
+ }
8
+ export interface WriterOptions {
9
+ /** Frames; past this the writer warns (once per stall episode) to stderr. */
10
+ highWaterMark?: number;
11
+ /** Sink for the high-water warning (defaults to a `process.stderr` write). */
12
+ warn?: (message: string) => void;
13
+ }
14
+ export declare class BoundedNdjsonWriter<T = ServerFrame> {
15
+ private readonly sink;
16
+ private readonly opts;
17
+ private readonly queue;
18
+ private draining;
19
+ private closed;
20
+ private warnedHighWater;
21
+ private readonly flushWaiters;
22
+ constructor(sink: ByteSink, opts?: WriterOptions);
23
+ /** Serialise + enqueue a frame. Synchronous (callable from a sync bus
24
+ * handler); the async drain loop does the actual writing. The type param
25
+ * lets the bespoke transport keep `ServerFrame` (the default) while the M3
26
+ * ACP transport enqueues JSON-RPC messages through the SAME bounded queue. */
27
+ enqueue(frame: T): void;
28
+ private drain;
29
+ /** Resolves once the queue has fully drained (or the writer is closed). */
30
+ flush(): Promise<void>;
31
+ /** Stop accepting + draining frames (transport closing). */
32
+ close(): void;
33
+ }
34
+ //# sourceMappingURL=writer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../src/rpc/writer.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;yCACyC;AACzC,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAID,qBAAa,mBAAmB,CAAC,CAAC,GAAG,WAAW;IAQ5C,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IARvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAyB;gBAGnC,IAAI,EAAE,QAAQ,EACd,IAAI,GAAE,aAAkB;IAG3C;;;kFAG8E;IAC9E,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI;YAaT,KAAK;IA2BnB,2EAA2E;IACrE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B,4DAA4D;IAC5D,KAAK,IAAI,IAAI;CAMd"}
@@ -0,0 +1,85 @@
1
+ // Single bounded FIFO writer (codex C4 + Round-1 S1). Every outbound frame
2
+ // goes through one ordered queue, so a `response` / `request_permission` can
3
+ // never overtake a queued `assistant_delta`, and the drain loop respects the
4
+ // sink's backpressure (`write()` → false → await "drain"). Fixes the
5
+ // sync-unawaited-write flaw in pi/oh-my-pi. Never drops a frame; a stalled
6
+ // client grows the in-memory queue and trips a re-armable stderr warning past
7
+ // a high-water mark (no silent cap).
8
+ const DEFAULT_HIGH_WATER = 10_000;
9
+ export class BoundedNdjsonWriter {
10
+ sink;
11
+ opts;
12
+ queue = [];
13
+ draining = false;
14
+ closed = false;
15
+ warnedHighWater = false;
16
+ flushWaiters = [];
17
+ constructor(sink, opts = {}) {
18
+ this.sink = sink;
19
+ this.opts = opts;
20
+ }
21
+ /** Serialise + enqueue a frame. Synchronous (callable from a sync bus
22
+ * handler); the async drain loop does the actual writing. The type param
23
+ * lets the bespoke transport keep `ServerFrame` (the default) while the M3
24
+ * ACP transport enqueues JSON-RPC messages through the SAME bounded queue. */
25
+ enqueue(frame) {
26
+ if (this.closed)
27
+ return;
28
+ this.queue.push(JSON.stringify(frame) + "\n");
29
+ const hwm = this.opts.highWaterMark ?? DEFAULT_HIGH_WATER;
30
+ if (this.queue.length > hwm && !this.warnedHighWater) {
31
+ this.warnedHighWater = true;
32
+ (this.opts.warn ?? defaultWarn)(`rpc: outbound queue exceeded ${hwm} frames — client may be stalled; not dropping`);
33
+ }
34
+ void this.drain();
35
+ }
36
+ async drain() {
37
+ if (this.draining)
38
+ return;
39
+ this.draining = true;
40
+ try {
41
+ while (this.queue.length > 0 && !this.closed) {
42
+ const line = this.queue.shift();
43
+ let ok;
44
+ try {
45
+ ok = this.sink.write(line);
46
+ }
47
+ catch {
48
+ // EPIPE / sink gone — stop writing, drop the rest cleanly.
49
+ this.closed = true;
50
+ this.queue.length = 0;
51
+ break;
52
+ }
53
+ if (!ok && !this.closed) {
54
+ await new Promise((resolve) => this.sink.once("drain", resolve));
55
+ }
56
+ }
57
+ }
58
+ finally {
59
+ this.draining = false;
60
+ if (this.queue.length === 0)
61
+ this.warnedHighWater = false; // re-arm
62
+ const waiters = this.flushWaiters.splice(0);
63
+ for (const w of waiters)
64
+ w();
65
+ }
66
+ }
67
+ /** Resolves once the queue has fully drained (or the writer is closed). */
68
+ async flush() {
69
+ if (this.closed || (this.queue.length === 0 && !this.draining))
70
+ return;
71
+ await new Promise((resolve) => this.flushWaiters.push(resolve));
72
+ }
73
+ /** Stop accepting + draining frames (transport closing). */
74
+ close() {
75
+ this.closed = true;
76
+ this.queue.length = 0;
77
+ const waiters = this.flushWaiters.splice(0);
78
+ for (const w of waiters)
79
+ w();
80
+ }
81
+ }
82
+ function defaultWarn(message) {
83
+ process.stderr.write(message + "\n");
84
+ }
85
+ //# sourceMappingURL=writer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer.js","sourceRoot":"","sources":["../../src/rpc/writer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * (v20 M3 / docs/6.4a §3; v21 M4 lease / docs/6.5 §5) `AcpSessionHost` — the ACP
3
+ * twin of the bespoke session host. ONE persistent engine session per process,
4
+ * sockets attach/detach over a seq-stamping `ReplayHub` fan-out; the inner engine
5
+ * is the `AcpEngineDriver` (ACP JSON-RPC wire).
6
+ *
7
+ * Two deliberate differences from the M2 design:
8
+ * 1. **State source.** The reconnect snapshot (`busy` / `pendingPermissionIds`)
9
+ * is read straight off the driver (authoritative). The `ReplayHub` stays a
10
+ * pure payload-agnostic seq/replay/fan-out layer — it does NOT parse the
11
+ * outbound bytes for session state (a byte-snoop lived there in the
12
+ * chances-rpc era; removed with the M3 hard cutover, since ACP frames' first
13
+ * key is `jsonrpc`/`rseq`, never `type`).
14
+ * 2. **No `ready` re-send.** ACP is client-initiated: a fresh client sends
15
+ * `initialize`/`session/new` and the driver replies; a cold-reloaded client
16
+ * re-initializes (client-core, c4).
17
+ *
18
+ * **Controller lease (v21 M4 §5).** Disabled by default ⇒ fan-out: any paired
19
+ * device drives, the engine serializes turns via `AGENT_BUSY` (the M2 model).
20
+ * When enabled, this host gates every inbound command by its source clientId —
21
+ * only the lease HOLDER may drive (`session/prompt`/`cancel`), answer a
22
+ * permission, or change the model; everyone else is a read-only VIEWER and gets a
23
+ * typed `not_controller`. `_chances/unstable/take_control` hands control over
24
+ * (any authed client may claim it — single-user multi-device) and bumps
25
+ * `controlEpoch`; the new holder + epoch are broadcast as a
26
+ * `_chances/unstable/control` notification. The gate lives HERE (it needs the
27
+ * socket→clientId map) so the driver stays a protocol-pure turn runner.
28
+ *
29
+ * **Threat model (codex M4-review).** The lease is COORDINATION among one user's
30
+ * equally-authed devices, NOT an authorization boundary. The security boundary is
31
+ * the pairing token (every connection is already token-authed); `clientId` is a
32
+ * self-reported coordination tag, not a trusted identity. A device could spoof
33
+ * another's clientId to "impersonate" the holder — but it gains nothing
34
+ * `take_control` doesn't already grant (any authed client may claim control), so
35
+ * there is no privilege escalation among your own devices. A truly-restricted
36
+ * viewer (read-only access shared with a THIRD party) needs server-issued identity
37
+ * and belongs to the multi-tenant Team era — out of scope here.
38
+ *
39
+ * The seq/replay layer (`ReplayHub`) is reused UNCHANGED — it stamps `rseq` on
40
+ * the serialized ACP JSON-RPC bytes (payload-agnostic, docs/6.2 §3.2).
41
+ */
42
+ import { type EngineHost } from "../rpc/index.js";
43
+ import { type ReplayHubOptions } from "./replay-hub.js";
44
+ import { type ServerSocket } from "./ws-transport.js";
45
+ export interface AcpSessionHostOptions {
46
+ host: EngineHost;
47
+ agent: {
48
+ name: string;
49
+ version: string;
50
+ };
51
+ /** Auto-approve every tool permission (trusted automation). Default false. */
52
+ autoApprove?: boolean;
53
+ /** stderr-style sink for routed `log` events + writer diagnostics. */
54
+ logSink?: (line: string) => void;
55
+ /** Replay ring depth (frames). Default {@link ReplayHub}'s. */
56
+ ring?: ReplayHubOptions;
57
+ /** (v21 M4 §5) Enable the single-controller lease. Default false ⇒ fan-out
58
+ * (any paired device drives; the engine serializes turns via AGENT_BUSY). */
59
+ controllerLease?: boolean;
60
+ }
61
+ export interface AcpAttachment {
62
+ /** Per-attach fencing token (forward-compat; not enforced on the M3 WS — the
63
+ * fan-out has no exclusive lock to fence). */
64
+ epoch: number;
65
+ /** Remove this socket from the fan-out. Idempotent; does NOT end the session. */
66
+ detach(): void;
67
+ }
68
+ export declare class AcpSessionHost {
69
+ private readonly hub;
70
+ private readonly queue;
71
+ private readonly driver;
72
+ private readonly host;
73
+ private readonly done;
74
+ private epochCounter;
75
+ private shuttingDown;
76
+ private readonly leaseEnabled;
77
+ private holder;
78
+ private controlEpoch;
79
+ /** clientId → socket, for TARGETED lease replies (not_controller / ack). */
80
+ private readonly socketsByClient;
81
+ constructor(opts: AcpSessionHostOptions);
82
+ /**
83
+ * Attach a socket, synchronously replaying everything since `lastSeq` then
84
+ * going live (same disjoint-and-ordered guarantee as the M2 host). `clientId`
85
+ * (from the `?client_id=` connect query) attributes this socket for the lease.
86
+ * Wire order: `relay_welcome` → replayed gap → re-sent open permissions → [live].
87
+ */
88
+ attach(socket: ServerSocket, lastSeq: number, clientId?: string): AcpAttachment;
89
+ /**
90
+ * One inbound text message (ACP JSON-RPC). Split LF-batched lines; each line
91
+ * passes the lease {@link gate} (a no-op when the lease is disabled) before
92
+ * reaching the ONE persistent queue. `fromClientId` (the attach's clientId)
93
+ * attributes the command to a client for the lease.
94
+ */
95
+ onMessage(text: string, fromClientId?: string): void;
96
+ /** Answer one read-only workspace query: dispatch to the host's query
97
+ * primitives and `replyTo` the originating socket only (off-ring). Needs an id
98
+ * (to respond) and a clientId (to target); a query missing either is dropped
99
+ * (a conformant client always sends both). */
100
+ private handleQuery;
101
+ /**
102
+ * Controller-lease gate. Returns true ⇒ the line may proceed to the driver;
103
+ * false ⇒ the host handled it (a handoff) or rejected it (a viewer's privileged
104
+ * command — a `not_controller` reply was sent to the source). A no-op (always
105
+ * true) when the lease is disabled.
106
+ */
107
+ private gate;
108
+ /** Broadcast the current lease state to every socket (stamped + fanned out). */
109
+ private broadcastControlState;
110
+ /** Send a single frame to ONE client (targeted; not fanned out, not stamped). */
111
+ private replyTo;
112
+ get headSeq(): number;
113
+ get socketCount(): number;
114
+ get busy(): boolean;
115
+ /** Current lease holder (clientId) or null. Exposed for tests/diagnostics. */
116
+ get controllerHolder(): string | null;
117
+ /** End the persistent queue → `run` returns → graceful driver shutdown. */
118
+ shutdown(): Promise<number>;
119
+ }
120
+ //# sourceMappingURL=acp-session-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acp-session-host.d.ts","sourceRoot":"","sources":["../../src/serve/acp-session-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EAKL,KAAK,UAAU,EAEhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAWpE,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,8EAA8E;IAC9E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,sEAAsE;IACtE,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,+DAA+D;IAC/D,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB;kFAC8E;IAC9E,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B;mDAC+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAY;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAkB;IACvC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,YAAY,CAAS;IAG7B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAAK;IACzB,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAmC;gBAEvD,IAAI,EAAE,qBAAqB;IAoBvC;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,aAAa;IAwC/E;;;;;OAKG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI;IAkBpD;;;mDAG+C;YACjC,WAAW;IASzB;;;;;OAKG;IACH,OAAO,CAAC,IAAI;IAuCZ,gFAAgF;IAChF,OAAO,CAAC,qBAAqB;IAK7B,iFAAiF;IACjF,OAAO,CAAC,OAAO;IAKf,IAAI,OAAO,IAAI,MAAM,CAEpB;IACD,IAAI,WAAW,IAAI,MAAM,CAExB;IACD,IAAI,IAAI,IAAI,OAAO,CAElB;IACD,8EAA8E;IAC9E,IAAI,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEpC;IAED,2EAA2E;IACrE,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;CAOlC"}