@rynx-ai/runtime 0.1.11-beta.37 → 0.1.11-beta.39
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/claude/native-bridge.d.ts +85 -0
- package/dist/claude/native-bridge.js +335 -17
- package/dist/claude/native-hook-main.js +18 -1
- package/dist/claude/native-hooks.js +7 -0
- package/dist/claude/native-integration.d.ts +120 -18
- package/dist/claude/native-integration.js +1200 -161
- package/dist/claude/transcript-clone.d.ts +18 -0
- package/dist/claude/transcript-clone.js +497 -0
- package/dist/claude/transcript.d.ts +27 -4
- package/dist/claude/transcript.js +131 -30
- package/dist/codex-session-store.d.ts +23 -0
- package/dist/codex-session-store.js +21 -0
- package/dist/host.d.ts +31 -2
- package/dist/host.js +551 -72
- package/dist/runner/child.d.ts +29 -5
- package/dist/runner/child.js +635 -54
- package/dist/runner/manager.d.ts +25 -0
- package/dist/runner/manager.js +805 -115
- package/dist/runner/protocol.d.ts +76 -3
- package/dist/runner/transport.d.ts +9 -0
- package/dist/runner/transport.js +39 -12
- package/package.json +2 -2
package/dist/runner/child.js
CHANGED
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
* needs an app-server of its own.
|
|
9
9
|
*/
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { appendFileSync, mkdirSync, renameSync, rmSync, statSync, } from "node:fs";
|
|
12
|
+
import { dirname } from "node:path";
|
|
11
13
|
import { AgentRuntimeError, } from "@rynx-ai/core";
|
|
12
14
|
import { TerminalRegistry, } from "../terminal/registry.js";
|
|
13
15
|
import { RUNNER_IMAGE_CHUNK_CHARS, RUNNER_IMAGE_MAX_RESULT_CHARS, toWireError, } from "./protocol.js";
|
|
14
16
|
import { isCodexLineageProvider } from "./startup-policy.js";
|
|
17
|
+
import { RunnerTransportWriteError, } from "./transport.js";
|
|
15
18
|
const TERMINAL_PREPARATION_TTL_MS = 30_000;
|
|
16
19
|
/** Maximum time for a fresh Codex-lineage TUI to publish its native thread. */
|
|
17
20
|
const CODEX_THREAD_START_TIMEOUT_MS = 30_000;
|
|
@@ -54,6 +57,68 @@ const TRAEX_ESCAPE_CONFIRM_MS = 1_000;
|
|
|
54
57
|
const TRAEX_ESCAPE_COOLDOWN_MS = 1_000;
|
|
55
58
|
const TRAEX_REPEAT_PROMPT_CONFIRM_MS = 5_000;
|
|
56
59
|
const MIRROR_IMAGE_ACK_TIMEOUT_MS = 30_000;
|
|
60
|
+
const MIRROR_ACK_TIMEOUT_MS = 30_000;
|
|
61
|
+
const MIRROR_DEAD_LETTER_MAX_BYTES = 50 * 1024 * 1024;
|
|
62
|
+
class MirrorDeliveryError extends Error {
|
|
63
|
+
classification;
|
|
64
|
+
deliveryId;
|
|
65
|
+
constructor(message, classification, deliveryId) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.classification = classification;
|
|
68
|
+
this.deliveryId = deliveryId;
|
|
69
|
+
this.name = "MirrorDeliveryError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
class MirrorGenerationSupersededError extends Error {
|
|
73
|
+
generation;
|
|
74
|
+
deliveryId;
|
|
75
|
+
constructor(generation, deliveryId) {
|
|
76
|
+
super(`mirror generation ${generation} was superseded by Session rotation`);
|
|
77
|
+
this.generation = generation;
|
|
78
|
+
this.deliveryId = deliveryId;
|
|
79
|
+
this.name = "MirrorGenerationSupersededError";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Delivery retry schedule: 1, 2, 4, 8, 16, 30, 30… seconds.
|
|
83
|
+
* Clamp before exponentiation so an indefinitely transient outage cannot
|
|
84
|
+
* overflow after thousands of attempts. */
|
|
85
|
+
export function mirrorRetryDelayMs(failedAttempt) {
|
|
86
|
+
const exponent = Math.min(Math.max(failedAttempt - 1, 0), 32);
|
|
87
|
+
return Math.min(2 ** exponent, 30) * 1_000;
|
|
88
|
+
}
|
|
89
|
+
function defaultMirrorDeliveryPolicy(event) {
|
|
90
|
+
switch (event.type) {
|
|
91
|
+
case "response.output_item.done":
|
|
92
|
+
case "session.input.consumed":
|
|
93
|
+
case "session.interaction.requested":
|
|
94
|
+
case "session.interaction.resolved":
|
|
95
|
+
case "session.interaction.cancelled":
|
|
96
|
+
return "ordinary";
|
|
97
|
+
default:
|
|
98
|
+
return "best-effort";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function appendMirrorDeadLetter(path, entry) {
|
|
102
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
103
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
104
|
+
let currentBytes = 0;
|
|
105
|
+
try {
|
|
106
|
+
currentBytes = statSync(path).size;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// absent
|
|
110
|
+
}
|
|
111
|
+
if (currentBytes >= MIRROR_DEAD_LETTER_MAX_BYTES) {
|
|
112
|
+
rmSync(`${path}.1`, { force: true });
|
|
113
|
+
try {
|
|
114
|
+
renameSync(path, `${path}.1`);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// absent/raced — append creates the active file below
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
appendFileSync(path, line, { mode: 0o600 });
|
|
121
|
+
}
|
|
57
122
|
function normalizeTraexPane(pane) {
|
|
58
123
|
return pane.toLowerCase().replace(/\s+/g, " ").trim();
|
|
59
124
|
}
|
|
@@ -133,11 +198,17 @@ export class RunnerSession {
|
|
|
133
198
|
cancelledTerminalOpens = new Set();
|
|
134
199
|
/** Session ids with a live codex forwarder started here (stopped on shutdown). */
|
|
135
200
|
liveIds = new Set();
|
|
136
|
-
|
|
201
|
+
mirrorQueues = new Map();
|
|
202
|
+
mirrorAcks = new Map();
|
|
137
203
|
mirrorImageAcks = new Map();
|
|
204
|
+
mirrorRetryWaiters = new Map();
|
|
205
|
+
rotationPublishAcks = new Map();
|
|
206
|
+
rotationAppliedAcks = new Map();
|
|
138
207
|
/** Provider name retained for asynchronous Terminal-exit diagnostics. */
|
|
139
208
|
liveRuntimes = new Map();
|
|
140
209
|
terminalStatuses = new Map();
|
|
210
|
+
consecutiveMirrorFailures = 0;
|
|
211
|
+
mirrorDegraded = false;
|
|
141
212
|
shuttingDown = false;
|
|
142
213
|
shutdownPromise = null;
|
|
143
214
|
constructor({ transport, executor, onShutdown }) {
|
|
@@ -150,16 +221,39 @@ export class RunnerSession {
|
|
|
150
221
|
});
|
|
151
222
|
this.transport.onMessage((msg) => this.handle(msg));
|
|
152
223
|
this.transport.onClose((error) => {
|
|
224
|
+
this.rejectMirrorAcks(new MirrorDeliveryError(error?.message ?? "runner transport closed before mirror acknowledgement", "ambiguous"));
|
|
153
225
|
this.rejectMirrorImageAcks(error ?? new Error("runner transport closed"));
|
|
226
|
+
this.rejectRotationAcks(error ?? new Error("runner transport closed"));
|
|
154
227
|
});
|
|
155
228
|
this.transport.send({ t: "ready" });
|
|
156
229
|
}
|
|
157
230
|
handle(msg) {
|
|
158
231
|
switch (msg.t) {
|
|
232
|
+
case "mirror.ack": {
|
|
233
|
+
const pending = this.mirrorAcks.get(msg.deliveryId);
|
|
234
|
+
if (!pending || msg.generation !== pending.generation) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
clearTimeout(pending.timer);
|
|
238
|
+
this.mirrorAcks.delete(msg.deliveryId);
|
|
239
|
+
pending.resolve();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
case "mirror.nack": {
|
|
243
|
+
const pending = this.mirrorAcks.get(msg.deliveryId);
|
|
244
|
+
if (!pending || msg.generation !== pending.generation) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
clearTimeout(pending.timer);
|
|
248
|
+
this.mirrorAcks.delete(msg.deliveryId);
|
|
249
|
+
pending.reject(new MirrorDeliveryError(msg.message, msg.classification, msg.deliveryId));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
159
252
|
case "mirror.image.ack": {
|
|
160
253
|
const pending = this.mirrorImageAcks.get(msg.transferId);
|
|
161
|
-
if (!pending)
|
|
254
|
+
if (!pending || msg.generation !== pending.generation) {
|
|
162
255
|
return;
|
|
256
|
+
}
|
|
163
257
|
if (pending.seq !== msg.seq) {
|
|
164
258
|
pending.reject(new Error("generated image acknowledgement sequence changed"));
|
|
165
259
|
return;
|
|
@@ -172,13 +266,54 @@ export class RunnerSession {
|
|
|
172
266
|
}
|
|
173
267
|
case "mirror.image.hold": {
|
|
174
268
|
const pending = this.mirrorImageAcks.get(msg.transferId);
|
|
175
|
-
if (!pending ||
|
|
269
|
+
if (!pending ||
|
|
270
|
+
pending.seq !== msg.seq ||
|
|
271
|
+
msg.generation !== pending.generation)
|
|
176
272
|
return;
|
|
177
273
|
if (pending.timer)
|
|
178
274
|
clearTimeout(pending.timer);
|
|
179
275
|
pending.timer = undefined;
|
|
180
276
|
return;
|
|
181
277
|
}
|
|
278
|
+
case "mirror.image.nack": {
|
|
279
|
+
const pending = this.mirrorImageAcks.get(msg.transferId);
|
|
280
|
+
if (!pending ||
|
|
281
|
+
pending.seq !== msg.seq ||
|
|
282
|
+
msg.generation !== pending.generation)
|
|
283
|
+
return;
|
|
284
|
+
if (pending.timer)
|
|
285
|
+
clearTimeout(pending.timer);
|
|
286
|
+
this.mirrorImageAcks.delete(msg.transferId);
|
|
287
|
+
pending.reject(new MirrorDeliveryError(msg.message, msg.classification));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
case "rotate.ack": {
|
|
291
|
+
const pending = this.rotationPublishAcks.get(msg.rotationId);
|
|
292
|
+
if (!pending || pending.generation !== msg.generation)
|
|
293
|
+
return;
|
|
294
|
+
clearTimeout(pending.timer);
|
|
295
|
+
this.rotationPublishAcks.delete(msg.rotationId);
|
|
296
|
+
pending.resolve();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
case "rotate.nack": {
|
|
300
|
+
const pending = this.rotationPublishAcks.get(msg.rotationId);
|
|
301
|
+
if (!pending || pending.generation !== msg.generation)
|
|
302
|
+
return;
|
|
303
|
+
clearTimeout(pending.timer);
|
|
304
|
+
this.rotationPublishAcks.delete(msg.rotationId);
|
|
305
|
+
pending.reject(new MirrorDeliveryError(msg.message, msg.classification));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
case "rotate.applied.ack": {
|
|
309
|
+
const pending = this.rotationAppliedAcks.get(msg.rotationId);
|
|
310
|
+
if (!pending || pending.generation !== msg.generation)
|
|
311
|
+
return;
|
|
312
|
+
clearTimeout(pending.timer);
|
|
313
|
+
this.rotationAppliedAcks.delete(msg.rotationId);
|
|
314
|
+
pending.resolve();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
182
317
|
case "cap":
|
|
183
318
|
void this.runCap(msg.capId, msg.name, msg.args);
|
|
184
319
|
return;
|
|
@@ -283,29 +418,109 @@ export class RunnerSession {
|
|
|
283
418
|
* new session AND tells the daemon to alias the runner (terminal transfer) so
|
|
284
419
|
* the new session stays injectable. */
|
|
285
420
|
mirrorChannel(localThreadId) {
|
|
286
|
-
const target = { id: localThreadId };
|
|
287
|
-
|
|
421
|
+
const target = { id: localThreadId, generation: 0 };
|
|
422
|
+
let pendingRetarget;
|
|
423
|
+
const emit = (event, delivery) => {
|
|
288
424
|
const sessionId = target.id;
|
|
425
|
+
const generation = target.generation;
|
|
289
426
|
if (event.type === "session.status") {
|
|
290
427
|
this.terminalStatuses.set(sessionId, event.status);
|
|
291
428
|
}
|
|
292
|
-
this.enqueueMirror(() => this.sendMirroredEvent(sessionId, event));
|
|
429
|
+
return this.enqueueMirror(delivery?.lane ?? "main", () => this.sendMirroredEvent(sessionId, event, delivery, generation, () => generation === target.generation));
|
|
293
430
|
};
|
|
294
431
|
const retarget = (newId, meta) => {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
432
|
+
let publication = pendingRetarget;
|
|
433
|
+
if (!publication && target.id === newId)
|
|
434
|
+
return Promise.resolve();
|
|
435
|
+
if (publication && publication.to !== newId) {
|
|
436
|
+
return Promise.reject(new Error(`Session rotation to ${publication.to} is still pending; cannot retarget to ${newId}`));
|
|
437
|
+
}
|
|
438
|
+
if (!publication) {
|
|
439
|
+
const previousGeneration = target.generation;
|
|
440
|
+
publication = {
|
|
441
|
+
from: target.id,
|
|
302
442
|
to: newId,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
443
|
+
generation: previousGeneration + 1,
|
|
444
|
+
rotationId: `rotate_${randomUUID()}`,
|
|
445
|
+
meta,
|
|
446
|
+
};
|
|
447
|
+
pendingRetarget = publication;
|
|
448
|
+
// Fence every source delivery immediately, but do not mutate the target
|
|
449
|
+
// identity or transfer resources until the parent ACKs publication.
|
|
450
|
+
target.generation = publication.generation;
|
|
451
|
+
this.cancelMirrorGeneration(previousGeneration);
|
|
452
|
+
}
|
|
453
|
+
if (publication.operation)
|
|
454
|
+
return publication.operation;
|
|
455
|
+
const current = publication;
|
|
456
|
+
const operation = this.enqueueMirror("control", async () => {
|
|
457
|
+
if (!current.resourcesApplied) {
|
|
458
|
+
let attempt = 0;
|
|
459
|
+
let publicationAcked = false;
|
|
460
|
+
while (!publicationAcked) {
|
|
461
|
+
attempt += 1;
|
|
462
|
+
try {
|
|
463
|
+
await this.sendRotationFrame(current.rotationId, current.generation, {
|
|
464
|
+
t: "rotate",
|
|
465
|
+
rotationId: current.rotationId,
|
|
466
|
+
generation: current.generation,
|
|
467
|
+
from: current.from,
|
|
468
|
+
to: current.to,
|
|
469
|
+
kind: current.meta.kind,
|
|
470
|
+
workspace: current.meta.workspace,
|
|
471
|
+
execution: current.meta.execution,
|
|
472
|
+
...(current.meta.parentSessionId
|
|
473
|
+
? { parentSessionId: current.meta.parentSessionId }
|
|
474
|
+
: {}),
|
|
475
|
+
});
|
|
476
|
+
publicationAcked = true;
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
const failure = error instanceof MirrorDeliveryError
|
|
480
|
+
? error
|
|
481
|
+
: new MirrorDeliveryError(error instanceof Error ? error.message : String(error), "transient");
|
|
482
|
+
if (failure.classification === "permanent" && attempt >= 3) {
|
|
483
|
+
// The parent still owns source/target reservations for this
|
|
484
|
+
// half-published cutover. End the child so failHandle releases
|
|
485
|
+
// them; leaving a live fenced pane would deadlock both ids.
|
|
486
|
+
await this.shutdown();
|
|
487
|
+
throw failure;
|
|
488
|
+
}
|
|
489
|
+
await this.waitForMirrorRetry(attempt, current.generation);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (pendingRetarget !== current)
|
|
493
|
+
return;
|
|
494
|
+
this.transferNativeSessionResources(current.from, current.to);
|
|
495
|
+
target.id = current.to;
|
|
496
|
+
current.resourcesApplied = true;
|
|
497
|
+
}
|
|
498
|
+
if (pendingRetarget !== current)
|
|
499
|
+
return;
|
|
500
|
+
if (!current.hostApplied) {
|
|
501
|
+
await current.meta.beforeApplied?.();
|
|
502
|
+
if (pendingRetarget !== current)
|
|
503
|
+
return;
|
|
504
|
+
current.hostApplied = true;
|
|
505
|
+
}
|
|
506
|
+
// Ordered after the actual child-side ownership transfer. The parent
|
|
507
|
+
// keeps source and target admissions reserved until it observes this.
|
|
508
|
+
await this.sendRotationAppliedFrame(current.rotationId, current.generation, {
|
|
509
|
+
t: "rotate.applied",
|
|
510
|
+
rotationId: current.rotationId,
|
|
511
|
+
generation: current.generation,
|
|
512
|
+
from: current.from,
|
|
513
|
+
to: current.to,
|
|
307
514
|
});
|
|
515
|
+
if (pendingRetarget !== current)
|
|
516
|
+
return;
|
|
517
|
+
pendingRetarget = undefined;
|
|
518
|
+
}, { recoverable: true }).finally(() => {
|
|
519
|
+
if (pendingRetarget === current)
|
|
520
|
+
delete current.operation;
|
|
308
521
|
});
|
|
522
|
+
current.operation = operation;
|
|
523
|
+
return operation;
|
|
309
524
|
};
|
|
310
525
|
return { emit, retarget };
|
|
311
526
|
}
|
|
@@ -339,62 +554,386 @@ export class RunnerSession {
|
|
|
339
554
|
if (terminal)
|
|
340
555
|
this.watchNativeTerminal(targetId, targetTerminalId, terminal);
|
|
341
556
|
}
|
|
342
|
-
enqueueMirror(operation) {
|
|
343
|
-
|
|
344
|
-
|
|
557
|
+
enqueueMirror(lane, operation, options = {}) {
|
|
558
|
+
const previous = this.mirrorQueues.get(lane) ?? Promise.resolve();
|
|
559
|
+
const queued = previous.then(operation);
|
|
560
|
+
const tail = queued.catch((error) => {
|
|
561
|
+
if (!this.shuttingDown && !options.recoverable) {
|
|
345
562
|
this.rejectMirrorImageAcks(error instanceof Error ? error : new Error(String(error)));
|
|
346
563
|
void this.shutdown();
|
|
347
564
|
}
|
|
348
565
|
});
|
|
566
|
+
this.mirrorQueues.set(lane, tail);
|
|
567
|
+
void tail.finally(() => {
|
|
568
|
+
if (this.mirrorQueues.get(lane) === tail)
|
|
569
|
+
this.mirrorQueues.delete(lane);
|
|
570
|
+
});
|
|
571
|
+
return queued;
|
|
349
572
|
}
|
|
350
|
-
async sendMirroredEvent(sessionId, event) {
|
|
351
|
-
|
|
352
|
-
if (!detached) {
|
|
353
|
-
this.transport.send({ t: "mirror", sessionId, event });
|
|
573
|
+
async sendMirroredEvent(sessionId, event, delivery, generation = 0, isCurrent = () => true) {
|
|
574
|
+
if (!isCurrent())
|
|
354
575
|
return;
|
|
576
|
+
try {
|
|
577
|
+
const detached = detachGeneratedImageResult(event);
|
|
578
|
+
if (!detached) {
|
|
579
|
+
await this.deliverMirrorEvent(sessionId, event, delivery, generation, isCurrent);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (detached.result.length > RUNNER_IMAGE_MAX_RESULT_CHARS) {
|
|
583
|
+
await this.deliverMirrorEvent(sessionId, generatedImagePreviewUnavailable(detached.event), delivery, generation, isCurrent);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
await this.deliverMirrorImageEvent(sessionId, detached.event, detached.result, delivery, generation, isCurrent);
|
|
355
587
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
event: generatedImagePreviewUnavailable(detached.event),
|
|
361
|
-
});
|
|
362
|
-
return;
|
|
588
|
+
catch (error) {
|
|
589
|
+
if (error instanceof MirrorGenerationSupersededError)
|
|
590
|
+
return;
|
|
591
|
+
throw error;
|
|
363
592
|
}
|
|
593
|
+
}
|
|
594
|
+
async deliverMirrorImageEvent(sessionId, event, result, delivery, generation, isCurrent) {
|
|
364
595
|
const transferId = `img_${randomUUID()}`;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
t: "mirror.image.begin",
|
|
596
|
+
const abandon = () => this.transport.sendAndDrain({
|
|
597
|
+
t: "mirror.image.abandon",
|
|
368
598
|
transferId,
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
599
|
+
generation,
|
|
600
|
+
}).catch(() => {
|
|
601
|
+
// Parent/child teardown already releases process-local transfer state.
|
|
372
602
|
});
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
await this.
|
|
376
|
-
t: "mirror.image.
|
|
603
|
+
let seq = 0;
|
|
604
|
+
try {
|
|
605
|
+
await this.deliverMirrorImageTransportFrame(transferId, seq, generation, {
|
|
606
|
+
t: "mirror.image.begin",
|
|
377
607
|
transferId,
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
608
|
+
sessionId,
|
|
609
|
+
event,
|
|
610
|
+
totalChars: result.length,
|
|
611
|
+
generation,
|
|
612
|
+
}, isCurrent);
|
|
613
|
+
for (let offset = 0; offset < result.length; offset += RUNNER_IMAGE_CHUNK_CHARS) {
|
|
614
|
+
if (!isCurrent())
|
|
615
|
+
throw new MirrorGenerationSupersededError(generation);
|
|
616
|
+
seq += 1;
|
|
617
|
+
await this.deliverMirrorImageTransportFrame(transferId, seq, generation, {
|
|
618
|
+
t: "mirror.image.chunk",
|
|
619
|
+
transferId,
|
|
620
|
+
seq,
|
|
621
|
+
data: result.slice(offset, offset + RUNNER_IMAGE_CHUNK_CHARS),
|
|
622
|
+
generation,
|
|
623
|
+
}, isCurrent);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
await abandon();
|
|
628
|
+
throw error;
|
|
381
629
|
}
|
|
382
630
|
seq += 1;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
631
|
+
const policy = delivery?.policy ?? defaultMirrorDeliveryPolicy(event);
|
|
632
|
+
let attempt = 0;
|
|
633
|
+
while (!this.shuttingDown) {
|
|
634
|
+
if (!isCurrent()) {
|
|
635
|
+
delivery?.onOutcome?.("superseded");
|
|
636
|
+
await abandon();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
attempt += 1;
|
|
640
|
+
try {
|
|
641
|
+
await this.sendMirrorImageFrame(transferId, seq, generation, {
|
|
642
|
+
t: "mirror.image.commit",
|
|
643
|
+
transferId,
|
|
644
|
+
seq,
|
|
645
|
+
generation,
|
|
646
|
+
}, "ambiguous");
|
|
647
|
+
this.noteMirrorDeliveryRecovered();
|
|
648
|
+
delivery?.onOutcome?.("confirmed");
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
if (error instanceof MirrorGenerationSupersededError || !isCurrent()) {
|
|
653
|
+
delivery?.onOutcome?.("superseded");
|
|
654
|
+
await abandon();
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (this.shuttingDown)
|
|
658
|
+
break;
|
|
659
|
+
const failure = error instanceof MirrorDeliveryError
|
|
660
|
+
? error
|
|
661
|
+
: new MirrorDeliveryError(error instanceof Error ? error.message : String(error), "ambiguous");
|
|
662
|
+
this.noteMirrorDeliveryFailed(failure);
|
|
663
|
+
if (policy === "best-effort") {
|
|
664
|
+
delivery?.onOutcome?.("dropped");
|
|
665
|
+
await abandon();
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (failure.classification === "ambiguous" &&
|
|
669
|
+
(policy === "ordinary" || policy === "compaction" || policy === "compaction-hook")) {
|
|
670
|
+
this.noteMirrorDeliveryRecovered();
|
|
671
|
+
delivery?.onOutcome?.("ambiguous");
|
|
672
|
+
await abandon();
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (failure.classification === "permanent" &&
|
|
676
|
+
policy !== "compaction" &&
|
|
677
|
+
attempt >= 3) {
|
|
678
|
+
if (policy === "ordinary" && delivery?.deadLetterPath) {
|
|
679
|
+
try {
|
|
680
|
+
appendMirrorDeadLetter(delivery.deadLetterPath, {
|
|
681
|
+
ts: Date.now() / 1_000,
|
|
682
|
+
session_id: sessionId,
|
|
683
|
+
event_type: "session_event",
|
|
684
|
+
reason: "permanent IPC persistence failure after retries",
|
|
685
|
+
delivered_ambiguous: false,
|
|
686
|
+
http_status: null,
|
|
687
|
+
transport_error: null,
|
|
688
|
+
payload: {
|
|
689
|
+
source_id: delivery.sourceId,
|
|
690
|
+
attempts: attempt,
|
|
691
|
+
event,
|
|
692
|
+
generated_image_result: result,
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
catch (deadLetterError) {
|
|
697
|
+
// The dead-letter writer is observability-only and never
|
|
698
|
+
// turns an exhausted poison item into a runner-wide failure.
|
|
699
|
+
console.error("[mirror] failed to append dead letter:", deadLetterError);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
delivery?.onOutcome?.("dropped");
|
|
703
|
+
await abandon();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
await this.waitForMirrorRetry(attempt, generation);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
await abandon();
|
|
710
|
+
throw new MirrorDeliveryError("runner shut down before generated image delivery was handled", "transient");
|
|
711
|
+
}
|
|
712
|
+
async deliverMirrorImageTransportFrame(transferId, seq, generation, frame, isCurrent) {
|
|
713
|
+
let attempt = 0;
|
|
714
|
+
while (!this.shuttingDown) {
|
|
715
|
+
if (!isCurrent())
|
|
716
|
+
throw new MirrorGenerationSupersededError(generation);
|
|
717
|
+
attempt += 1;
|
|
718
|
+
try {
|
|
719
|
+
await this.sendMirrorImageFrame(transferId, seq, generation, frame, "transient");
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
if (error instanceof MirrorGenerationSupersededError || !isCurrent())
|
|
724
|
+
throw error;
|
|
725
|
+
const failure = error instanceof MirrorDeliveryError
|
|
726
|
+
? error
|
|
727
|
+
: new MirrorDeliveryError(error instanceof Error ? error.message : String(error), "transient");
|
|
728
|
+
this.noteMirrorDeliveryFailed(failure);
|
|
729
|
+
await this.waitForMirrorRetry(attempt, generation);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
throw new MirrorDeliveryError("runner shut down during generated image transfer", "transient");
|
|
733
|
+
}
|
|
734
|
+
async deliverMirrorEvent(sessionId, event, delivery, generation = 0, isCurrent = () => true) {
|
|
735
|
+
const policy = delivery?.policy ?? defaultMirrorDeliveryPolicy(event);
|
|
736
|
+
let attempt = 0;
|
|
737
|
+
let lastFailedDeliveryId;
|
|
738
|
+
while (!this.shuttingDown) {
|
|
739
|
+
if (!isCurrent()) {
|
|
740
|
+
delivery?.onOutcome?.("superseded");
|
|
741
|
+
await this.abandonMirrorDelivery(lastFailedDeliveryId, generation);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
attempt += 1;
|
|
745
|
+
try {
|
|
746
|
+
await this.sendMirrorEventFrame(sessionId, event, generation, attempt);
|
|
747
|
+
this.noteMirrorDeliveryRecovered();
|
|
748
|
+
delivery?.onOutcome?.("confirmed");
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
catch (error) {
|
|
752
|
+
if (error instanceof MirrorGenerationSupersededError || !isCurrent()) {
|
|
753
|
+
delivery?.onOutcome?.("superseded");
|
|
754
|
+
await this.abandonMirrorDelivery(error instanceof MirrorGenerationSupersededError
|
|
755
|
+
? error.deliveryId
|
|
756
|
+
: error instanceof MirrorDeliveryError
|
|
757
|
+
? error.deliveryId
|
|
758
|
+
: lastFailedDeliveryId, generation);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (this.shuttingDown)
|
|
762
|
+
break;
|
|
763
|
+
const failure = error instanceof MirrorDeliveryError
|
|
764
|
+
? error
|
|
765
|
+
: new MirrorDeliveryError(error instanceof Error ? error.message : String(error), "ambiguous");
|
|
766
|
+
lastFailedDeliveryId = failure.deliveryId;
|
|
767
|
+
this.noteMirrorDeliveryFailed(failure);
|
|
768
|
+
if (policy === "best-effort") {
|
|
769
|
+
console.warn(`[mirror] best-effort event dropped: ${failure.message}`);
|
|
770
|
+
delivery?.onOutcome?.("dropped");
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (failure.classification === "ambiguous" &&
|
|
774
|
+
(policy === "ordinary" || policy === "compaction" || policy === "compaction-hook")) {
|
|
775
|
+
console.warn(`[mirror] ordinary delivery result is ambiguous; treating as handled: ${failure.message}`);
|
|
776
|
+
this.noteMirrorDeliveryRecovered();
|
|
777
|
+
delivery?.onOutcome?.("ambiguous");
|
|
778
|
+
await this.abandonMirrorDelivery(failure.deliveryId, generation);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (failure.classification === "permanent" &&
|
|
782
|
+
policy !== "compaction" &&
|
|
783
|
+
attempt >= 3) {
|
|
784
|
+
if (policy === "ordinary" && delivery?.deadLetterPath) {
|
|
785
|
+
try {
|
|
786
|
+
appendMirrorDeadLetter(delivery.deadLetterPath, {
|
|
787
|
+
ts: Date.now() / 1_000,
|
|
788
|
+
session_id: sessionId,
|
|
789
|
+
event_type: "session_event",
|
|
790
|
+
reason: "permanent IPC persistence failure after retries",
|
|
791
|
+
delivered_ambiguous: false,
|
|
792
|
+
http_status: null,
|
|
793
|
+
transport_error: null,
|
|
794
|
+
payload: {
|
|
795
|
+
source_id: delivery.sourceId,
|
|
796
|
+
attempts: attempt,
|
|
797
|
+
event,
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
catch (deadLetterError) {
|
|
802
|
+
console.error("[mirror] failed to append dead letter:", deadLetterError);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
console.error(`[mirror] permanent delivery failed after ${attempt} attempts; skipping: ${failure.message}`);
|
|
806
|
+
delivery?.onOutcome?.("dropped");
|
|
807
|
+
await this.abandonMirrorDelivery(failure.deliveryId, generation);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
await this.waitForMirrorRetry(attempt, generation);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
throw new MirrorDeliveryError("runner shut down before mirror delivery was handled", "transient");
|
|
814
|
+
}
|
|
815
|
+
noteMirrorDeliveryFailed(error) {
|
|
816
|
+
this.consecutiveMirrorFailures += 1;
|
|
817
|
+
if (!this.mirrorDegraded && this.consecutiveMirrorFailures >= 5) {
|
|
818
|
+
this.mirrorDegraded = true;
|
|
819
|
+
console.warn(`[mirror] delivery degraded after ${this.consecutiveMirrorFailures} consecutive failures: ${error.message}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
noteMirrorDeliveryRecovered() {
|
|
823
|
+
if (this.mirrorDegraded)
|
|
824
|
+
console.info("[mirror] delivery recovered");
|
|
825
|
+
this.consecutiveMirrorFailures = 0;
|
|
826
|
+
this.mirrorDegraded = false;
|
|
827
|
+
}
|
|
828
|
+
sendMirrorEventFrame(sessionId, event, generation, attempt) {
|
|
829
|
+
const deliveryId = `mirror_${randomUUID()}`;
|
|
830
|
+
return new Promise((resolve, reject) => {
|
|
831
|
+
const timer = setTimeout(() => {
|
|
832
|
+
this.mirrorAcks.delete(deliveryId);
|
|
833
|
+
reject(new MirrorDeliveryError("mirror acknowledgement timed out", "ambiguous", deliveryId));
|
|
834
|
+
}, MIRROR_ACK_TIMEOUT_MS);
|
|
835
|
+
timer.unref?.();
|
|
836
|
+
this.mirrorAcks.set(deliveryId, { generation, resolve, reject, timer });
|
|
837
|
+
void this.transport.sendAndDrain({
|
|
838
|
+
t: "mirror",
|
|
839
|
+
deliveryId,
|
|
840
|
+
generation,
|
|
841
|
+
attempt,
|
|
842
|
+
sessionId,
|
|
843
|
+
event,
|
|
844
|
+
}).catch((error) => {
|
|
845
|
+
const pending = this.mirrorAcks.get(deliveryId);
|
|
846
|
+
if (!pending)
|
|
847
|
+
return;
|
|
848
|
+
clearTimeout(pending.timer);
|
|
849
|
+
this.mirrorAcks.delete(deliveryId);
|
|
850
|
+
const classification = error instanceof RunnerTransportWriteError &&
|
|
851
|
+
error.delivery === "not-sent"
|
|
852
|
+
? "transient"
|
|
853
|
+
: "ambiguous";
|
|
854
|
+
pending.reject(new MirrorDeliveryError(error instanceof Error ? error.message : String(error), classification, deliveryId));
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
async abandonMirrorDelivery(deliveryId, generation) {
|
|
859
|
+
if (!deliveryId)
|
|
860
|
+
return;
|
|
861
|
+
await this.transport.sendAndDrain({
|
|
862
|
+
t: "mirror.abandon",
|
|
863
|
+
deliveryId,
|
|
864
|
+
generation,
|
|
865
|
+
}).catch(() => {
|
|
866
|
+
// Parent/child teardown already releases process-local projection state.
|
|
387
867
|
});
|
|
388
868
|
}
|
|
389
|
-
|
|
869
|
+
waitForMirrorRetry(failedAttempt, generation) {
|
|
870
|
+
return new Promise((resolve) => {
|
|
871
|
+
const waiters = this.mirrorRetryWaiters.get(generation) ?? new Set();
|
|
872
|
+
let timer;
|
|
873
|
+
const done = () => {
|
|
874
|
+
clearTimeout(timer);
|
|
875
|
+
waiters.delete(done);
|
|
876
|
+
if (waiters.size === 0)
|
|
877
|
+
this.mirrorRetryWaiters.delete(generation);
|
|
878
|
+
resolve();
|
|
879
|
+
};
|
|
880
|
+
timer = setTimeout(done, mirrorRetryDelayMs(failedAttempt));
|
|
881
|
+
timer.unref?.();
|
|
882
|
+
waiters.add(done);
|
|
883
|
+
this.mirrorRetryWaiters.set(generation, waiters);
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
cancelMirrorGeneration(generation) {
|
|
887
|
+
const error = new MirrorGenerationSupersededError(generation);
|
|
888
|
+
for (const [deliveryId, pending] of this.mirrorAcks) {
|
|
889
|
+
if (pending.generation !== generation)
|
|
890
|
+
continue;
|
|
891
|
+
clearTimeout(pending.timer);
|
|
892
|
+
this.mirrorAcks.delete(deliveryId);
|
|
893
|
+
pending.reject(new MirrorGenerationSupersededError(generation, deliveryId));
|
|
894
|
+
}
|
|
895
|
+
for (const [transferId, pending] of this.mirrorImageAcks) {
|
|
896
|
+
if (pending.generation !== generation)
|
|
897
|
+
continue;
|
|
898
|
+
if (pending.timer)
|
|
899
|
+
clearTimeout(pending.timer);
|
|
900
|
+
this.mirrorImageAcks.delete(transferId);
|
|
901
|
+
pending.reject(error);
|
|
902
|
+
}
|
|
903
|
+
for (const wake of this.mirrorRetryWaiters.get(generation) ?? [])
|
|
904
|
+
wake();
|
|
905
|
+
this.mirrorRetryWaiters.delete(generation);
|
|
906
|
+
}
|
|
907
|
+
rejectMirrorAcks(error) {
|
|
908
|
+
for (const pending of this.mirrorAcks.values()) {
|
|
909
|
+
clearTimeout(pending.timer);
|
|
910
|
+
pending.reject(error);
|
|
911
|
+
}
|
|
912
|
+
this.mirrorAcks.clear();
|
|
913
|
+
}
|
|
914
|
+
sendMirrorImageFrame(transferId, seq, generation, frame, timeoutClassification = "transient") {
|
|
390
915
|
return new Promise((resolve, reject) => {
|
|
916
|
+
const pending = { generation, seq, resolve, reject };
|
|
391
917
|
const timer = setTimeout(() => {
|
|
918
|
+
if (this.mirrorImageAcks.get(transferId) !== pending)
|
|
919
|
+
return;
|
|
392
920
|
this.mirrorImageAcks.delete(transferId);
|
|
393
|
-
reject(new
|
|
921
|
+
reject(new MirrorDeliveryError("generated image transfer acknowledgement timed out", timeoutClassification));
|
|
394
922
|
}, MIRROR_IMAGE_ACK_TIMEOUT_MS);
|
|
395
923
|
timer.unref?.();
|
|
396
|
-
|
|
397
|
-
this.
|
|
924
|
+
pending.timer = timer;
|
|
925
|
+
this.mirrorImageAcks.set(transferId, pending);
|
|
926
|
+
void this.transport.sendAndDrain(frame).catch((error) => {
|
|
927
|
+
if (this.mirrorImageAcks.get(transferId) !== pending)
|
|
928
|
+
return;
|
|
929
|
+
clearTimeout(timer);
|
|
930
|
+
this.mirrorImageAcks.delete(transferId);
|
|
931
|
+
const classification = timeoutClassification === "ambiguous" &&
|
|
932
|
+
(!(error instanceof RunnerTransportWriteError) || error.delivery === "ambiguous")
|
|
933
|
+
? "ambiguous"
|
|
934
|
+
: "transient";
|
|
935
|
+
reject(new MirrorDeliveryError(error instanceof Error ? error.message : String(error), classification));
|
|
936
|
+
});
|
|
398
937
|
});
|
|
399
938
|
}
|
|
400
939
|
rejectMirrorImageAcks(error) {
|
|
@@ -405,6 +944,40 @@ export class RunnerSession {
|
|
|
405
944
|
}
|
|
406
945
|
this.mirrorImageAcks.clear();
|
|
407
946
|
}
|
|
947
|
+
sendRotationFrame(rotationId, generation, frame) {
|
|
948
|
+
return new Promise((resolve, reject) => {
|
|
949
|
+
const timer = setTimeout(() => {
|
|
950
|
+
this.rotationPublishAcks.delete(rotationId);
|
|
951
|
+
reject(new Error("Session rotation acknowledgement timed out"));
|
|
952
|
+
}, MIRROR_ACK_TIMEOUT_MS);
|
|
953
|
+
timer.unref?.();
|
|
954
|
+
this.rotationPublishAcks.set(rotationId, { generation, resolve, reject, timer });
|
|
955
|
+
this.transport.send(frame);
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
sendRotationAppliedFrame(rotationId, generation, frame) {
|
|
959
|
+
return new Promise((resolve, reject) => {
|
|
960
|
+
const timer = setTimeout(() => {
|
|
961
|
+
this.rotationAppliedAcks.delete(rotationId);
|
|
962
|
+
reject(new Error("Session rotation applied acknowledgement timed out"));
|
|
963
|
+
}, MIRROR_ACK_TIMEOUT_MS);
|
|
964
|
+
timer.unref?.();
|
|
965
|
+
this.rotationAppliedAcks.set(rotationId, { generation, resolve, reject, timer });
|
|
966
|
+
this.transport.send(frame);
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
rejectRotationAcks(error) {
|
|
970
|
+
for (const pending of this.rotationPublishAcks.values()) {
|
|
971
|
+
clearTimeout(pending.timer);
|
|
972
|
+
pending.reject(error);
|
|
973
|
+
}
|
|
974
|
+
this.rotationPublishAcks.clear();
|
|
975
|
+
for (const pending of this.rotationAppliedAcks.values()) {
|
|
976
|
+
clearTimeout(pending.timer);
|
|
977
|
+
pending.reject(error);
|
|
978
|
+
}
|
|
979
|
+
this.rotationAppliedAcks.clear();
|
|
980
|
+
}
|
|
408
981
|
/**
|
|
409
982
|
* Eagerly bring up a session's codex-native live view. Fresh sessions connect
|
|
410
983
|
* the discovery listener before launching the detached TUI; known-thread
|
|
@@ -819,7 +1392,7 @@ export class RunnerSession {
|
|
|
819
1392
|
this.liveRuntimes.delete(localThreadId);
|
|
820
1393
|
this.terminalStatuses.delete(localThreadId);
|
|
821
1394
|
}
|
|
822
|
-
this.enqueueMirror(() => {
|
|
1395
|
+
this.enqueueMirror("main", () => {
|
|
823
1396
|
this.transport.send({
|
|
824
1397
|
t: "terminal.lifecycle.ended",
|
|
825
1398
|
localThreadId,
|
|
@@ -984,7 +1557,15 @@ export class RunnerSession {
|
|
|
984
1557
|
return this.shutdownPromise;
|
|
985
1558
|
this.shuttingDown = true;
|
|
986
1559
|
this.shutdownPromise = (async () => {
|
|
987
|
-
|
|
1560
|
+
const shutdownError = new Error("runner is shutting down");
|
|
1561
|
+
this.rejectMirrorAcks(shutdownError);
|
|
1562
|
+
this.rejectMirrorImageAcks(shutdownError);
|
|
1563
|
+
this.rejectRotationAcks(shutdownError);
|
|
1564
|
+
for (const waiters of this.mirrorRetryWaiters.values()) {
|
|
1565
|
+
for (const wake of waiters)
|
|
1566
|
+
wake();
|
|
1567
|
+
}
|
|
1568
|
+
this.mirrorRetryWaiters.clear();
|
|
988
1569
|
this.stopLive();
|
|
989
1570
|
for (const pending of this.preparations.values()) {
|
|
990
1571
|
clearTimeout(pending.timer);
|