@rynx-ai/runtime 0.1.11-beta.3 → 0.1.11-beta.30
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/executor.d.ts +19 -5
- package/dist/claude/executor.js +56 -12
- package/dist/claude/models.d.ts +0 -5
- package/dist/claude/models.js +1 -7
- package/dist/claude/native-bridge.d.ts +2 -0
- package/dist/claude/native-bridge.js +23 -0
- package/dist/claude/native-hook-main.js +62 -0
- package/dist/claude/native-integration.d.ts +50 -10
- package/dist/claude/native-integration.js +262 -37
- package/dist/claude/session-status.d.ts +39 -0
- package/dist/claude/session-status.js +163 -0
- package/dist/claude/transcript.js +27 -17
- package/dist/codex-app-server/client.d.ts +10 -6
- package/dist/codex-app-server/client.js +67 -15
- package/dist/codex-app-server/forwarder.d.ts +92 -3
- package/dist/codex-app-server/forwarder.js +509 -56
- package/dist/codex-app-server/mapping.d.ts +3 -6
- package/dist/codex-app-server/mapping.js +174 -28
- package/dist/codex-app-server/mcp-startup.d.ts +13 -0
- package/dist/codex-app-server/mcp-startup.js +63 -0
- package/dist/codex-app-server/protocol.d.ts +64 -7
- package/dist/codex-app-server/ws-channel.js +19 -19
- package/dist/codex-home.js +2 -4
- package/dist/host.d.ts +64 -21
- package/dist/host.js +1330 -441
- package/dist/index.d.ts +1 -1
- package/dist/input-resources.d.ts +4 -0
- package/dist/input-resources.js +21 -5
- package/dist/models-catalog.d.ts +2 -1
- package/dist/models-catalog.js +94 -6
- package/dist/runner/child.d.ts +48 -21
- package/dist/runner/child.js +550 -48
- package/dist/runner/manager.d.ts +54 -13
- package/dist/runner/manager.js +479 -114
- package/dist/runner/protocol.d.ts +62 -19
- package/dist/runner/protocol.js +5 -0
- package/dist/runner/startup-policy.d.ts +7 -0
- package/dist/runner/startup-policy.js +10 -0
- package/dist/terminal/claude-tui.d.ts +3 -1
- package/dist/terminal/claude-tui.js +3 -1
- package/dist/terminal/registry.js +3 -2
- package/dist/terminal/tmux.d.ts +50 -7
- package/dist/terminal/tmux.js +168 -47
- package/package.json +4 -3
package/dist/runner/manager.js
CHANGED
|
@@ -22,8 +22,9 @@ import { fileURLToPath } from "node:url";
|
|
|
22
22
|
import { AgentRuntimeError, } from "@rynx-ai/core";
|
|
23
23
|
import { listRuntimeModels } from "../models-catalog.js";
|
|
24
24
|
import { probeRuntimeStatus } from "../runtime-status.js";
|
|
25
|
-
import { terminateTmuxServer } from "../terminal/tmux.js";
|
|
26
|
-
import { fromWireError, } from "./protocol.js";
|
|
25
|
+
import { terminateTmuxServer, tmuxHasAttachedClient, tmuxWindowActivityAt, } from "../terminal/tmux.js";
|
|
26
|
+
import { RUNNER_IMAGE_CHUNK_CHARS, RUNNER_IMAGE_MAX_RESULT_CHARS, fromWireError, } from "./protocol.js";
|
|
27
|
+
import { isCodexLineageProvider, isManagedNativeProvider } from "./startup-policy.js";
|
|
27
28
|
import { StdioRunnerTransport } from "./transport.js";
|
|
28
29
|
/** Routing key for the shared capability runner (slash-command RPCs). */
|
|
29
30
|
const CAP_KEY = "__cap__";
|
|
@@ -33,17 +34,23 @@ const STDERR_TAIL_LINES = 40;
|
|
|
33
34
|
const DEFAULT_SHUTDOWN_GRACE_MS = 5_000;
|
|
34
35
|
/** Time allowed for exit after SIGKILL before shutdown reports failure. */
|
|
35
36
|
const DEFAULT_SHUTDOWN_KILL_GRACE_MS = 5_000;
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
/** Legacy setup-pane acknowledgement deadline for providers that do not own
|
|
38
|
+
* phase-specific startup errors. Codex/Traex are deliberately excluded. */
|
|
39
|
+
const DEFAULT_LIVE_START_TIMEOUT_MS = 30_000;
|
|
40
|
+
/** Legacy fallback for a provider without a native startup policy. */
|
|
39
41
|
const DEFAULT_LIVE_READY_TIMEOUT_MS = 25_000;
|
|
42
|
+
/** Claude has no app-server bridge and keeps a parent-owned SessionStart
|
|
43
|
+
* deadline. Codex-lineage startup instead acknowledges pane/observer startup
|
|
44
|
+
* and lets thread discovery race injection. */
|
|
45
|
+
const DEFAULT_NATIVE_LIVE_START_TIMEOUT_MS = 60_000;
|
|
40
46
|
/** A submitted TUI command should become a mirrored turn or native rotation
|
|
41
47
|
* quickly. If it does not, the runner is fenced by a verified process-tree
|
|
42
48
|
* shutdown before maintenance may treat the submission as settled. */
|
|
43
49
|
const DEFAULT_TERMINAL_INPUT_HANDOFF_TIMEOUT_MS = 30_000;
|
|
44
|
-
/**
|
|
45
|
-
const
|
|
46
|
-
|
|
50
|
+
/** Full idle window before an inactive native pane becomes reapable. */
|
|
51
|
+
const DEFAULT_NATIVE_PANE_IDLE_TTL_MS = 60 * 60_000;
|
|
52
|
+
/** tmux output this recent independently proves that a native pane is busy. */
|
|
53
|
+
const DEFAULT_NATIVE_PANE_OUTPUT_BUSY_WINDOW_MS = 120_000;
|
|
47
54
|
/** Keep per-Session spawn context small enough to remain an environment handoff,
|
|
48
55
|
* not an unbounded transport. */
|
|
49
56
|
const MAX_SESSION_CONTEXT_ENV_ENTRIES = 32;
|
|
@@ -144,6 +151,8 @@ export class RunnerManager {
|
|
|
144
151
|
sessionStore;
|
|
145
152
|
runnerEntry;
|
|
146
153
|
idleTtlMs;
|
|
154
|
+
nativePaneIdleTtlMs;
|
|
155
|
+
nativePaneOutputBusyWindowMs;
|
|
147
156
|
spawn;
|
|
148
157
|
childEnv;
|
|
149
158
|
sessionContextProvider;
|
|
@@ -160,15 +169,22 @@ export class RunnerManager {
|
|
|
160
169
|
shutdownKillGraceMs;
|
|
161
170
|
signalChild;
|
|
162
171
|
terminateTerminalServer;
|
|
172
|
+
terminalWindowActivityAt;
|
|
173
|
+
terminalHasAttachedClient;
|
|
174
|
+
wallNow;
|
|
163
175
|
liveStartTimeoutMs;
|
|
164
176
|
liveReadyTimeoutMs;
|
|
177
|
+
nativeLiveStartTimeoutMs;
|
|
165
178
|
liveInterruptTimeoutMs;
|
|
166
179
|
terminalInputHandoffTimeoutMs;
|
|
167
|
-
|
|
180
|
+
reapPromise = null;
|
|
168
181
|
stopping = false;
|
|
169
182
|
stopPromise;
|
|
170
183
|
/** Sink for mirrored {@link SessionEvent}s from every session's forwarder. */
|
|
171
184
|
mirrorListener = null;
|
|
185
|
+
/** Synchronous persistence hook for native collaboration-mode reflection.
|
|
186
|
+
* It runs before the event reaches the Session bus. */
|
|
187
|
+
collaborationModeListener = null;
|
|
172
188
|
/** Sink for session rotations (claude `/clear`·`/fork`) — server records meta. */
|
|
173
189
|
rotateListener = null;
|
|
174
190
|
/** Session keys with a live codex forwarder — never reaped while present. */
|
|
@@ -198,25 +214,30 @@ export class RunnerManager {
|
|
|
198
214
|
this.sessionStore = opts.sessionStore;
|
|
199
215
|
this.runnerEntry = opts.runnerEntry ?? defaultRunnerEntry();
|
|
200
216
|
this.idleTtlMs = opts.idleTtlMs ?? 300_000;
|
|
217
|
+
this.nativePaneIdleTtlMs = opts.nativePaneIdleTtlMs ?? DEFAULT_NATIVE_PANE_IDLE_TTL_MS;
|
|
218
|
+
this.nativePaneOutputBusyWindowMs = Math.max(0, opts.nativePaneOutputBusyWindowMs ?? DEFAULT_NATIVE_PANE_OUTPUT_BUSY_WINDOW_MS);
|
|
201
219
|
this.spawn = opts.spawn ?? nodeSpawn;
|
|
202
220
|
this.childEnv = opts.childEnv ?? {};
|
|
203
221
|
this.sessionContextProvider = opts.sessionContextProvider;
|
|
204
222
|
this.admissionOpen = opts.admissionOpen ?? (() => true);
|
|
205
223
|
this.admissionReserve = opts.admissionReserve;
|
|
206
224
|
this.now = opts.now ?? (() => Date.now());
|
|
225
|
+
this.wallNow = opts.wallNow ?? (() => Date.now());
|
|
207
226
|
this.defaultRuntime = opts.config.DEFAULT_RUNTIME ?? "codex";
|
|
208
227
|
this.shutdownGraceMs = Math.max(0, opts.shutdownGraceMs ?? DEFAULT_SHUTDOWN_GRACE_MS);
|
|
209
228
|
this.shutdownKillGraceMs = Math.max(0, opts.shutdownKillGraceMs ?? DEFAULT_SHUTDOWN_KILL_GRACE_MS);
|
|
210
229
|
this.signalChild = opts.signalChild ?? signalRunnerChild;
|
|
211
230
|
this.terminateTerminalServer = opts.terminateTerminalServer ?? terminateTmuxServer;
|
|
231
|
+
this.terminalWindowActivityAt = opts.terminalWindowActivityAt ?? tmuxWindowActivityAt;
|
|
232
|
+
this.terminalHasAttachedClient = opts.terminalHasAttachedClient ?? tmuxHasAttachedClient;
|
|
212
233
|
this.liveStartTimeoutMs = Math.max(1, opts.liveStartTimeoutMs ?? DEFAULT_LIVE_START_TIMEOUT_MS);
|
|
213
234
|
this.liveReadyTimeoutMs = Math.max(1, opts.liveReadyTimeoutMs ?? DEFAULT_LIVE_READY_TIMEOUT_MS);
|
|
235
|
+
this.nativeLiveStartTimeoutMs = Math.max(1, opts.nativeLiveStartTimeoutMs ?? DEFAULT_NATIVE_LIVE_START_TIMEOUT_MS);
|
|
214
236
|
this.liveInterruptTimeoutMs = Math.max(1, opts.liveInterruptTimeoutMs ?? 10_000);
|
|
215
237
|
this.terminalInputHandoffTimeoutMs = Math.max(1, opts.terminalInputHandoffTimeoutMs ?? DEFAULT_TERMINAL_INPUT_HANDOFF_TIMEOUT_MS);
|
|
216
|
-
this.terminalInputCleanupRetryMs = Math.max(1, opts.terminalInputCleanupRetryMs ?? DEFAULT_TERMINAL_INPUT_CLEANUP_RETRY_MS);
|
|
217
238
|
const reapIntervalMs = opts.reapIntervalMs ?? 60_000;
|
|
218
239
|
if (reapIntervalMs > 0) {
|
|
219
|
-
this.reapTimer = setInterval(() => this.reapIdle(), reapIntervalMs);
|
|
240
|
+
this.reapTimer = setInterval(() => void this.reapIdle(), reapIntervalMs);
|
|
220
241
|
this.reapTimer.unref?.();
|
|
221
242
|
}
|
|
222
243
|
else {
|
|
@@ -261,6 +282,8 @@ export class RunnerManager {
|
|
|
261
282
|
pendingEscape: "",
|
|
262
283
|
};
|
|
263
284
|
const terminal = new ManagedTerminal(attachId, (msg) => {
|
|
285
|
+
if (msg.t === "term.input")
|
|
286
|
+
handle.lastUsedAt = this.now();
|
|
264
287
|
const handoffs = msg.t === "term.input" && opts.role === "owner"
|
|
265
288
|
? this.beginTerminalInputHandoffs(handle, this.currentTerminalSessionId(handle, localThreadId), trackedTerminalSubmissions(inputTracker, Buffer.from(msg.dataB64, "base64").toString("utf8")))
|
|
266
289
|
: [];
|
|
@@ -329,6 +352,9 @@ export class RunnerManager {
|
|
|
329
352
|
onMirror(listener) {
|
|
330
353
|
this.mirrorListener = listener;
|
|
331
354
|
}
|
|
355
|
+
onCollaborationMode(listener) {
|
|
356
|
+
this.collaborationModeListener = listener;
|
|
357
|
+
}
|
|
332
358
|
/** Register the sink for session rotations (claude `/clear`·`/fork`): the server
|
|
333
359
|
* records the new session's meta (carry-over agent/model/title). */
|
|
334
360
|
onRotate(listener) {
|
|
@@ -431,11 +457,6 @@ export class RunnerManager {
|
|
|
431
457
|
this.terminalInputHandoffs.delete(handle);
|
|
432
458
|
}
|
|
433
459
|
finishAllTerminalInputHandoffs(handle) {
|
|
434
|
-
if (handle.terminalCleanupRetryTimer) {
|
|
435
|
-
clearTimeout(handle.terminalCleanupRetryTimer);
|
|
436
|
-
delete handle.terminalCleanupRetryTimer;
|
|
437
|
-
}
|
|
438
|
-
handle.terminalCleanupRetryFailures = 0;
|
|
439
460
|
for (const handoff of [...(this.terminalInputHandoffs.get(handle) ?? [])]) {
|
|
440
461
|
this.finishTerminalInputHandoff(handle, handoff);
|
|
441
462
|
}
|
|
@@ -450,22 +471,6 @@ export class RunnerManager {
|
|
|
450
471
|
handoff.reservation = undefined;
|
|
451
472
|
}
|
|
452
473
|
}
|
|
453
|
-
scheduleTerminalInputCleanupRetry(handle) {
|
|
454
|
-
if (handle.terminalCleanupRetryTimer ||
|
|
455
|
-
!this.terminalInputHandoffs.has(handle))
|
|
456
|
-
return;
|
|
457
|
-
const exponent = Math.min(handle.terminalCleanupRetryFailures - 1, 10);
|
|
458
|
-
const delay = Math.min(this.terminalInputCleanupRetryMs * (2 ** Math.max(0, exponent)), MAX_TERMINAL_INPUT_CLEANUP_RETRY_MS);
|
|
459
|
-
handle.terminalCleanupRetryTimer = setTimeout(() => {
|
|
460
|
-
delete handle.terminalCleanupRetryTimer;
|
|
461
|
-
if (!this.terminalInputHandoffs.has(handle))
|
|
462
|
-
return;
|
|
463
|
-
void this.terminateHandle(handle, "retrying unproven terminal input cleanup").catch((error) => {
|
|
464
|
-
logTerminationFailure(handle, error);
|
|
465
|
-
});
|
|
466
|
-
}, delay);
|
|
467
|
-
handle.terminalCleanupRetryTimer.unref?.();
|
|
468
|
-
}
|
|
469
474
|
expireTerminalInputHandoffs(handle) {
|
|
470
475
|
const handoffs = this.terminalInputHandoffs.get(handle);
|
|
471
476
|
if (!handoffs?.length)
|
|
@@ -483,8 +488,10 @@ export class RunnerManager {
|
|
|
483
488
|
/**
|
|
484
489
|
* Eagerly bring up a session's codex-native live view (persistent forwarder +
|
|
485
490
|
* detached `codex --remote` TUI) in its runner child, spawning the runner if
|
|
486
|
-
* needed. Idempotent.
|
|
487
|
-
*
|
|
491
|
+
* needed. Idempotent. For Codex-lineage sessions, resolves after backend
|
|
492
|
+
* preload and pane launch; known-thread observer attach and fresh-thread
|
|
493
|
+
* discovery continue in the background. Other providers retain their
|
|
494
|
+
* readiness gate.
|
|
488
495
|
*/
|
|
489
496
|
ensureLiveSession(localThreadId, opts) {
|
|
490
497
|
const admission = this.reserveAdmission();
|
|
@@ -523,42 +530,66 @@ export class RunnerManager {
|
|
|
523
530
|
this.liveSessionKeys.add(localThreadId);
|
|
524
531
|
const reqId = randomUUID();
|
|
525
532
|
return new Promise((resolve) => {
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
533
|
+
const provider = opts.execution.provider;
|
|
534
|
+
const timeoutMs = isCodexLineageProvider(provider)
|
|
535
|
+
? null
|
|
536
|
+
: !waitForReady
|
|
537
|
+
? this.liveStartTimeoutMs
|
|
538
|
+
: provider === "claude"
|
|
539
|
+
? this.nativeLiveStartTimeoutMs
|
|
540
|
+
: this.liveReadyTimeoutMs;
|
|
541
|
+
const timeout = timeoutMs === null
|
|
542
|
+
? undefined
|
|
543
|
+
: setTimeout(() => {
|
|
544
|
+
if (!handle.live.delete(reqId))
|
|
545
|
+
return;
|
|
546
|
+
const finishTimeout = () => {
|
|
547
|
+
const reason = !waitForReady
|
|
548
|
+
? `runner did not acknowledge terminal start within ${timeoutMs}ms`
|
|
549
|
+
: provider === "claude"
|
|
550
|
+
? `Claude SessionStart was not observed within ${Math.round(timeoutMs / 1_000)}s; the runner process was reset before retry`
|
|
551
|
+
: `runner did not acknowledge live readiness within ${timeoutMs}ms`;
|
|
552
|
+
this.liveErrors.set(localThreadId, {
|
|
553
|
+
message: reason,
|
|
554
|
+
code: provider === "claude"
|
|
555
|
+
? "claude_session_start_timeout"
|
|
556
|
+
: "native_runner_ack_timeout",
|
|
557
|
+
statusCode: 503,
|
|
558
|
+
});
|
|
559
|
+
this.liveSessionKeys.delete(localThreadId);
|
|
560
|
+
// Legacy providers without phase-owned startup errors retain the
|
|
561
|
+
// bounded control guard. Codex/Traex never enter this branch.
|
|
562
|
+
void this.terminateHandle(handle, reason).catch((error) => {
|
|
563
|
+
logTerminationFailure(handle, error);
|
|
564
|
+
});
|
|
565
|
+
return false;
|
|
566
|
+
};
|
|
567
|
+
const reservation = allowReservedForkTarget
|
|
568
|
+
? undefined
|
|
569
|
+
: this.forkReservations.get(localThreadId);
|
|
570
|
+
if (reservation) {
|
|
571
|
+
void reservation
|
|
572
|
+
.then(() => finishTimeout())
|
|
573
|
+
.then(resolve, () => resolve(false));
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
resolve(finishTimeout());
|
|
577
|
+
}, timeoutMs);
|
|
578
|
+
timeout?.unref?.();
|
|
553
579
|
handle.live.set(reqId, (res) => {
|
|
554
|
-
|
|
580
|
+
if (timeout)
|
|
581
|
+
clearTimeout(timeout);
|
|
555
582
|
const ok = res.ok ?? false;
|
|
556
583
|
const finish = () => {
|
|
557
584
|
if (ok) {
|
|
558
585
|
this.liveErrors.delete(localThreadId);
|
|
559
586
|
}
|
|
560
587
|
else {
|
|
561
|
-
this.liveErrors.set(localThreadId, res.error ??
|
|
588
|
+
this.liveErrors.set(localThreadId, res.error ?? {
|
|
589
|
+
message: "live session did not become ready",
|
|
590
|
+
code: "native_readiness_failed",
|
|
591
|
+
statusCode: 503,
|
|
592
|
+
});
|
|
562
593
|
}
|
|
563
594
|
return ok;
|
|
564
595
|
};
|
|
@@ -586,7 +617,11 @@ export class RunnerManager {
|
|
|
586
617
|
});
|
|
587
618
|
}
|
|
588
619
|
lastLiveSessionError(localThreadId) {
|
|
589
|
-
return this.liveErrors.get(localThreadId);
|
|
620
|
+
return this.liveErrors.get(localThreadId)?.message;
|
|
621
|
+
}
|
|
622
|
+
lastLiveSessionFailure(localThreadId) {
|
|
623
|
+
const failure = this.liveErrors.get(localThreadId);
|
|
624
|
+
return failure ? { ...failure } : undefined;
|
|
590
625
|
}
|
|
591
626
|
/**
|
|
592
627
|
* Inject a user turn into a session's live codex thread — reference implementation's
|
|
@@ -594,47 +629,55 @@ export class RunnerManager {
|
|
|
594
629
|
* all output. Resolves true when the app-server accepted the turn, false when
|
|
595
630
|
* the session has no live forwarder (caller falls back to the run path).
|
|
596
631
|
*/
|
|
597
|
-
injectMessage(localThreadId, input) {
|
|
632
|
+
injectMessage(localThreadId, input, options) {
|
|
598
633
|
const admission = this.reserveAdmission();
|
|
599
|
-
return this.injectMessageAdmitted(localThreadId, input)
|
|
634
|
+
return this.injectMessageAdmitted(localThreadId, input, options)
|
|
600
635
|
.finally(() => admission.release());
|
|
601
636
|
}
|
|
602
|
-
injectMessageAdmitted(localThreadId, input) {
|
|
637
|
+
injectMessageAdmitted(localThreadId, input, options) {
|
|
603
638
|
const sourceReservation = this.sourceForkReservations.get(localThreadId);
|
|
604
639
|
if (sourceReservation) {
|
|
605
|
-
return sourceReservation.then(() => this.injectMessageAdmitted(localThreadId, input));
|
|
640
|
+
return sourceReservation.then(() => this.injectMessageAdmitted(localThreadId, input, options));
|
|
606
641
|
}
|
|
607
642
|
const reservation = this.forkReservations.get(localThreadId);
|
|
608
643
|
if (reservation) {
|
|
609
|
-
return reservation.then(() => this.injectMessageAdmitted(localThreadId, input));
|
|
644
|
+
return reservation.then(() => this.injectMessageAdmitted(localThreadId, input, options));
|
|
610
645
|
}
|
|
611
646
|
const handle = this.getOrSpawn(localThreadId);
|
|
612
647
|
handle.lastUsedAt = this.now();
|
|
613
648
|
const reqId = randomUUID();
|
|
614
649
|
return new Promise((resolve) => {
|
|
615
650
|
handle.live.set(reqId, (res) => {
|
|
616
|
-
const
|
|
651
|
+
const result = res.result ?? { outcome: "failed" };
|
|
617
652
|
const finish = () => {
|
|
618
|
-
if (outcome === "injected") {
|
|
653
|
+
if (result.outcome === "injected" || result.outcome === "steered") {
|
|
619
654
|
this.liveErrors.delete(localThreadId);
|
|
620
655
|
}
|
|
621
656
|
else {
|
|
622
|
-
this.liveErrors.set(localThreadId, res.error ??
|
|
657
|
+
this.liveErrors.set(localThreadId, res.error ?? {
|
|
658
|
+
message: `live injection ${result.outcome}`,
|
|
659
|
+
code: result.outcome === "notReady"
|
|
660
|
+
? "native_thread_not_ready"
|
|
661
|
+
: result.outcome === "notLive"
|
|
662
|
+
? "native_session_not_live"
|
|
663
|
+
: "native_message_injection_failed",
|
|
664
|
+
statusCode: 503,
|
|
665
|
+
});
|
|
623
666
|
}
|
|
624
|
-
return
|
|
667
|
+
return result;
|
|
625
668
|
};
|
|
626
669
|
const reservation = this.forkReservations.get(localThreadId);
|
|
627
670
|
if (reservation) {
|
|
628
671
|
void reservation
|
|
629
672
|
.then(() => finish())
|
|
630
|
-
.then(resolve, () => resolve("failed"));
|
|
673
|
+
.then(resolve, () => resolve({ outcome: "failed" }));
|
|
631
674
|
return;
|
|
632
675
|
}
|
|
633
676
|
resolve(finish());
|
|
634
677
|
});
|
|
635
678
|
handle.transport.send(typeof input === "string"
|
|
636
|
-
? { t: "inject", reqId, localThreadId, text: input }
|
|
637
|
-
: { t: "inject", reqId, localThreadId, input });
|
|
679
|
+
? { t: "inject", reqId, localThreadId, text: input, ...(options ? { options } : {}) }
|
|
680
|
+
: { t: "inject", reqId, localThreadId, input, ...(options ? { options } : {}) });
|
|
638
681
|
});
|
|
639
682
|
}
|
|
640
683
|
/**
|
|
@@ -674,7 +717,7 @@ export class RunnerManager {
|
|
|
674
717
|
handle.live.set(reqId, (reply) => {
|
|
675
718
|
resolve(reply.interactionResult ?? {
|
|
676
719
|
disposition: "invalid",
|
|
677
|
-
message: reply.error ?? "runner did not return an interaction result",
|
|
720
|
+
message: reply.error?.message ?? "runner did not return an interaction result",
|
|
678
721
|
});
|
|
679
722
|
});
|
|
680
723
|
handle.transport.send({
|
|
@@ -686,6 +729,34 @@ export class RunnerManager {
|
|
|
686
729
|
});
|
|
687
730
|
});
|
|
688
731
|
}
|
|
732
|
+
/** Update a loaded Codex-lineage native thread without spawning a runner. */
|
|
733
|
+
updateCollaborationMode(localThreadId, mode) {
|
|
734
|
+
const handle = this.handles.get(localThreadId);
|
|
735
|
+
if (!handle || handle.dead || !this.liveSessionKeys.has(localThreadId)) {
|
|
736
|
+
return Promise.reject(new AgentRuntimeError("Native Session is not live", 409, "native_session_not_live"));
|
|
737
|
+
}
|
|
738
|
+
handle.lastUsedAt = this.now();
|
|
739
|
+
const reqId = randomUUID();
|
|
740
|
+
return new Promise((resolve, reject) => {
|
|
741
|
+
handle.live.set(reqId, (reply) => {
|
|
742
|
+
if (reply.collaborationUpdated) {
|
|
743
|
+
resolve();
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
reject(fromWireError(reply.error ?? {
|
|
747
|
+
message: "Native collaboration mode update failed",
|
|
748
|
+
code: "collaboration_mode_update_failed",
|
|
749
|
+
statusCode: 503,
|
|
750
|
+
}));
|
|
751
|
+
});
|
|
752
|
+
handle.transport.send({
|
|
753
|
+
t: "collaboration.update",
|
|
754
|
+
reqId,
|
|
755
|
+
localThreadId,
|
|
756
|
+
mode,
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
}
|
|
689
760
|
// ── AgentCapabilities ──────────────────────────────────────────────────────
|
|
690
761
|
async listModels(runtime) {
|
|
691
762
|
return listRuntimeModels(this.config, runtime ?? this.defaultRuntime);
|
|
@@ -869,7 +940,6 @@ export class RunnerManager {
|
|
|
869
940
|
!allowReservedForkTarget) {
|
|
870
941
|
throw new AgentRuntimeError("Session fork is still being committed", 409, "session_fork_pending");
|
|
871
942
|
}
|
|
872
|
-
this.reapIdle();
|
|
873
943
|
const existing = this.handles.get(key);
|
|
874
944
|
if (existing && !existing.dead) {
|
|
875
945
|
existing.lastUsedAt = this.now();
|
|
@@ -1011,14 +1081,56 @@ export class RunnerManager {
|
|
|
1011
1081
|
if (handle.dead)
|
|
1012
1082
|
return;
|
|
1013
1083
|
if (message.t === "mirror") {
|
|
1014
|
-
this.
|
|
1015
|
-
if (message.event.type === "response.created") {
|
|
1016
|
-
this.settleTerminalInputHandoff(handle, message.sessionId, "turn");
|
|
1017
|
-
}
|
|
1084
|
+
this.deliverMirrorMessage(handle, message);
|
|
1018
1085
|
return;
|
|
1019
1086
|
}
|
|
1020
1087
|
this.deliverRotateMessage(handle, message);
|
|
1021
1088
|
}
|
|
1089
|
+
deliverMirrorMessage(handle, message) {
|
|
1090
|
+
if (handle.dead)
|
|
1091
|
+
return;
|
|
1092
|
+
if (message.event.type === "session.collaboration_mode") {
|
|
1093
|
+
this.collaborationModeListener?.(message.sessionId, message.event.mode);
|
|
1094
|
+
}
|
|
1095
|
+
this.observeHandleRuntimeEvent(handle, message.event);
|
|
1096
|
+
let delivered;
|
|
1097
|
+
try {
|
|
1098
|
+
delivered = Promise.resolve(this.mirrorListener?.(message.sessionId, message.event));
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
delivered = Promise.reject(error);
|
|
1102
|
+
}
|
|
1103
|
+
if (message.event.type === "response.created") {
|
|
1104
|
+
// The listener projects response.created into SessionRuntimeIndex
|
|
1105
|
+
// synchronously. Only then may the accepted terminal reservation drain
|
|
1106
|
+
// into a maintenance activity snapshot.
|
|
1107
|
+
this.settleTerminalInputHandoff(handle, message.sessionId, "turn");
|
|
1108
|
+
}
|
|
1109
|
+
if (message.transferId && message.seq !== undefined) {
|
|
1110
|
+
void delivered.then(() => {
|
|
1111
|
+
if (!handle.dead) {
|
|
1112
|
+
handle.transport.send({
|
|
1113
|
+
t: "mirror.image.ack",
|
|
1114
|
+
transferId: message.transferId,
|
|
1115
|
+
seq: message.seq,
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
}, (error) => {
|
|
1119
|
+
this.failMirrorImageTransfer(handle, `generated image publication failed: ${errorMessage(error)}`);
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
else {
|
|
1123
|
+
void delivered.catch((error) => {
|
|
1124
|
+
console.error(`Session mirror publication failed: ${errorMessage(error)}`);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
failMirrorImageTransfer(handle, reason) {
|
|
1129
|
+
handle.imageTransfers.clear();
|
|
1130
|
+
void this.terminateHandle(handle, reason).catch((error) => {
|
|
1131
|
+
logTerminationFailure(handle, error);
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1022
1134
|
deliverRotateMessage(handle, message) {
|
|
1023
1135
|
if (this.forkReservations.has(message.to) ||
|
|
1024
1136
|
this.sourceForkReservations.has(message.from)) {
|
|
@@ -1078,6 +1190,7 @@ export class RunnerManager {
|
|
|
1078
1190
|
this.settleTerminalRotationHandoff(handle, message.from, message.to);
|
|
1079
1191
|
if (handle.dead)
|
|
1080
1192
|
return;
|
|
1193
|
+
handle.activeResponseIds.clear();
|
|
1081
1194
|
this.handles.set(message.to, handle);
|
|
1082
1195
|
this.liveSessionKeys.add(message.to);
|
|
1083
1196
|
this.liveOptions.set(message.to, {
|
|
@@ -1144,14 +1257,15 @@ export class RunnerManager {
|
|
|
1144
1257
|
transport,
|
|
1145
1258
|
stderr: [],
|
|
1146
1259
|
lastUsedAt: this.now(),
|
|
1260
|
+
activeResponseIds: new Set(),
|
|
1147
1261
|
dead: false,
|
|
1148
1262
|
completion,
|
|
1149
|
-
terminalCleanupRetryFailures: 0,
|
|
1150
1263
|
processGroup,
|
|
1151
1264
|
...(sessionContext ? { sessionContext } : {}),
|
|
1152
1265
|
caps: new Map(),
|
|
1153
1266
|
terminals: new Map(),
|
|
1154
1267
|
live: new Map(),
|
|
1268
|
+
imageTransfers: new Map(),
|
|
1155
1269
|
};
|
|
1156
1270
|
this.childHandles.add(handle);
|
|
1157
1271
|
void completion.then(() => this.childHandles.delete(handle));
|
|
@@ -1210,14 +1324,83 @@ export class RunnerManager {
|
|
|
1210
1324
|
return;
|
|
1211
1325
|
if (this.bufferForkTargetMessage(handle, msg))
|
|
1212
1326
|
return;
|
|
1213
|
-
this.
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1327
|
+
this.deliverMirrorMessage(handle, msg);
|
|
1328
|
+
return;
|
|
1329
|
+
case "mirror.image.begin": {
|
|
1330
|
+
if (handle.dead ||
|
|
1331
|
+
handle.imageTransfers.has(msg.transferId) ||
|
|
1332
|
+
!Number.isSafeInteger(msg.totalChars) ||
|
|
1333
|
+
msg.totalChars <= 0 ||
|
|
1334
|
+
msg.totalChars > RUNNER_IMAGE_MAX_RESULT_CHARS ||
|
|
1335
|
+
!isDetachedGeneratedImageEvent(msg.event)) {
|
|
1336
|
+
this.failMirrorImageTransfer(handle, "invalid generated image transfer start");
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
handle.imageTransfers.set(msg.transferId, {
|
|
1340
|
+
sessionId: msg.sessionId,
|
|
1341
|
+
event: msg.event,
|
|
1342
|
+
totalChars: msg.totalChars,
|
|
1343
|
+
receivedChars: 0,
|
|
1344
|
+
nextSeq: 1,
|
|
1345
|
+
chunks: [],
|
|
1346
|
+
});
|
|
1347
|
+
handle.transport.send({ t: "mirror.image.ack", transferId: msg.transferId, seq: 0 });
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
case "mirror.image.chunk": {
|
|
1351
|
+
const transfer = handle.imageTransfers.get(msg.transferId);
|
|
1352
|
+
if (handle.dead ||
|
|
1353
|
+
!transfer ||
|
|
1354
|
+
msg.seq !== transfer.nextSeq ||
|
|
1355
|
+
!msg.data ||
|
|
1356
|
+
msg.data.length > RUNNER_IMAGE_CHUNK_CHARS ||
|
|
1357
|
+
transfer.receivedChars + msg.data.length > transfer.totalChars) {
|
|
1358
|
+
this.failMirrorImageTransfer(handle, "invalid generated image transfer chunk");
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
transfer.chunks.push(msg.data);
|
|
1362
|
+
transfer.receivedChars += msg.data.length;
|
|
1363
|
+
transfer.nextSeq += 1;
|
|
1364
|
+
handle.transport.send({
|
|
1365
|
+
t: "mirror.image.ack",
|
|
1366
|
+
transferId: msg.transferId,
|
|
1367
|
+
seq: msg.seq,
|
|
1368
|
+
});
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
case "mirror.image.commit": {
|
|
1372
|
+
const transfer = handle.imageTransfers.get(msg.transferId);
|
|
1373
|
+
if (handle.dead ||
|
|
1374
|
+
!transfer ||
|
|
1375
|
+
msg.seq !== transfer.nextSeq ||
|
|
1376
|
+
transfer.receivedChars !== transfer.totalChars) {
|
|
1377
|
+
this.failMirrorImageTransfer(handle, "invalid generated image transfer commit");
|
|
1378
|
+
return;
|
|
1219
1379
|
}
|
|
1380
|
+
handle.imageTransfers.delete(msg.transferId);
|
|
1381
|
+
const event = attachGeneratedImageResult(transfer.event, transfer.chunks.join(""));
|
|
1382
|
+
if (!event) {
|
|
1383
|
+
this.failMirrorImageTransfer(handle, "generated image transfer target changed");
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
const mirrored = {
|
|
1387
|
+
t: "mirror",
|
|
1388
|
+
sessionId: transfer.sessionId,
|
|
1389
|
+
event,
|
|
1390
|
+
transferId: msg.transferId,
|
|
1391
|
+
seq: msg.seq,
|
|
1392
|
+
};
|
|
1393
|
+
if (this.bufferForkTargetMessage(handle, mirrored)) {
|
|
1394
|
+
handle.transport.send({
|
|
1395
|
+
t: "mirror.image.hold",
|
|
1396
|
+
transferId: msg.transferId,
|
|
1397
|
+
seq: msg.seq,
|
|
1398
|
+
});
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
this.deliverMirrorMessage(handle, mirrored);
|
|
1220
1402
|
return;
|
|
1403
|
+
}
|
|
1221
1404
|
case "rotate": {
|
|
1222
1405
|
if (handle.dead)
|
|
1223
1406
|
return;
|
|
@@ -1232,10 +1415,19 @@ export class RunnerManager {
|
|
|
1232
1415
|
const resolve = handle.live.get(msg.reqId);
|
|
1233
1416
|
handle.live.delete(msg.reqId);
|
|
1234
1417
|
resolve?.(msg.t === "injected"
|
|
1235
|
-
? {
|
|
1418
|
+
? { result: msg.result, error: msg.error }
|
|
1236
1419
|
: { ok: msg.ok, error: msg.error });
|
|
1237
1420
|
return;
|
|
1238
1421
|
}
|
|
1422
|
+
case "collaboration.updated": {
|
|
1423
|
+
const resolve = handle.live.get(msg.reqId);
|
|
1424
|
+
handle.live.delete(msg.reqId);
|
|
1425
|
+
resolve?.({
|
|
1426
|
+
collaborationUpdated: msg.ok,
|
|
1427
|
+
...(msg.error ? { error: msg.error } : {}),
|
|
1428
|
+
});
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1239
1431
|
case "interaction.resolved": {
|
|
1240
1432
|
const resolve = handle.live.get(msg.reqId);
|
|
1241
1433
|
handle.live.delete(msg.reqId);
|
|
@@ -1245,7 +1437,7 @@ export class RunnerManager {
|
|
|
1245
1437
|
}
|
|
1246
1438
|
}
|
|
1247
1439
|
/** Mark a handle dead and reject every pending run/cap with the exit reason. */
|
|
1248
|
-
failHandle(handle, reason) {
|
|
1440
|
+
failHandle(handle, reason, opts = {}) {
|
|
1249
1441
|
if (handle.dead) {
|
|
1250
1442
|
return;
|
|
1251
1443
|
}
|
|
@@ -1264,6 +1456,9 @@ export class RunnerManager {
|
|
|
1264
1456
|
}
|
|
1265
1457
|
const tail = handle.stderr.join("\n");
|
|
1266
1458
|
const message = tail ? `${reason}\n--- runner log tail ---\n${tail}` : reason;
|
|
1459
|
+
if (opts.failActiveResponses !== false) {
|
|
1460
|
+
this.failActiveResponses(handle, "runner_crashed", message);
|
|
1461
|
+
}
|
|
1267
1462
|
const error = new AgentRuntimeError(message, 500, "runner_crashed");
|
|
1268
1463
|
for (const pending of handle.caps.values()) {
|
|
1269
1464
|
pending.reject(error);
|
|
@@ -1272,20 +1467,20 @@ export class RunnerManager {
|
|
|
1272
1467
|
terminal._fail(message);
|
|
1273
1468
|
}
|
|
1274
1469
|
for (const resolve of handle.live.values()) {
|
|
1275
|
-
resolve({
|
|
1470
|
+
resolve({
|
|
1471
|
+
ok: false,
|
|
1472
|
+
error: { message, code: "runner_crashed", statusCode: 500 },
|
|
1473
|
+
});
|
|
1276
1474
|
}
|
|
1277
1475
|
handle.caps.clear();
|
|
1278
1476
|
handle.terminals.clear();
|
|
1279
1477
|
handle.live.clear();
|
|
1478
|
+
handle.imageTransfers.clear();
|
|
1280
1479
|
handle.transport.close();
|
|
1281
1480
|
}
|
|
1282
1481
|
terminateHandle(handle, reason) {
|
|
1283
1482
|
if (handle.termination)
|
|
1284
1483
|
return handle.termination;
|
|
1285
|
-
if (handle.terminalCleanupRetryTimer) {
|
|
1286
|
-
clearTimeout(handle.terminalCleanupRetryTimer);
|
|
1287
|
-
delete handle.terminalCleanupRetryTimer;
|
|
1288
|
-
}
|
|
1289
1484
|
const attempt = (async () => {
|
|
1290
1485
|
if (!handle.dead) {
|
|
1291
1486
|
try {
|
|
@@ -1294,7 +1489,8 @@ export class RunnerManager {
|
|
|
1294
1489
|
catch {
|
|
1295
1490
|
// The process signal below remains the authoritative shutdown path.
|
|
1296
1491
|
}
|
|
1297
|
-
this.
|
|
1492
|
+
this.interruptActiveResponses(handle);
|
|
1493
|
+
this.failHandle(handle, reason, { failActiveResponses: false });
|
|
1298
1494
|
}
|
|
1299
1495
|
this.signalChild(handle.child, "SIGTERM", handle.processGroup);
|
|
1300
1496
|
let childExited = await waitForChildExit(handle.completion, this.shutdownGraceMs);
|
|
@@ -1302,15 +1498,14 @@ export class RunnerManager {
|
|
|
1302
1498
|
this.signalChild(handle.child, "SIGKILL", handle.processGroup);
|
|
1303
1499
|
childExited = await waitForChildExit(handle.completion, this.shutdownKillGraceMs);
|
|
1304
1500
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1501
|
+
if (handle.key !== CAP_KEY) {
|
|
1502
|
+
await Promise.resolve(this.terminateTerminalServer(`${handle.key}-main`)).catch((error) => {
|
|
1503
|
+
console.warn(`[runner-manager] best-effort terminal close failed for ${handle.key}-main: ${error instanceof Error ? error.message : String(error)}`);
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1308
1506
|
if (!childExited) {
|
|
1309
1507
|
throw new Error(`runner child ${handle.child.pid ?? handle.key} did not exit after SIGKILL`);
|
|
1310
1508
|
}
|
|
1311
|
-
if (!terminalStopped) {
|
|
1312
|
-
throw new Error(`runner terminal ${handle.key}-main remained live after child exit`);
|
|
1313
|
-
}
|
|
1314
1509
|
this.childHandles.delete(handle);
|
|
1315
1510
|
})();
|
|
1316
1511
|
handle.termination = attempt;
|
|
@@ -1319,34 +1514,174 @@ export class RunnerManager {
|
|
|
1319
1514
|
}, () => {
|
|
1320
1515
|
if (handle.termination === attempt)
|
|
1321
1516
|
delete handle.termination;
|
|
1322
|
-
|
|
1323
|
-
//
|
|
1324
|
-
// submission in the daemon activity snapshot, but release its gate
|
|
1325
|
-
// reservation so maintenance returns a structured busy result.
|
|
1517
|
+
// A child that survives SIGKILL is still real activity. Release the
|
|
1518
|
+
// gate reservation but retain the handoff until a later explicit stop.
|
|
1326
1519
|
this.preserveTerminalInputHandoffsAsActivity(handle);
|
|
1327
|
-
this.scheduleTerminalInputCleanupRetry(handle);
|
|
1328
1520
|
});
|
|
1329
1521
|
return attempt;
|
|
1330
1522
|
}
|
|
1331
1523
|
reapIdle() {
|
|
1524
|
+
if (this.reapPromise)
|
|
1525
|
+
return this.reapPromise;
|
|
1526
|
+
const attempt = this.reapIdleOnce();
|
|
1527
|
+
this.reapPromise = attempt;
|
|
1528
|
+
const clear = () => {
|
|
1529
|
+
if (this.reapPromise === attempt)
|
|
1530
|
+
this.reapPromise = null;
|
|
1531
|
+
};
|
|
1532
|
+
void attempt.then(clear, clear);
|
|
1533
|
+
return attempt;
|
|
1534
|
+
}
|
|
1535
|
+
async reapIdleOnce() {
|
|
1332
1536
|
const now = this.now();
|
|
1333
1537
|
for (const [key, handle] of this.handles) {
|
|
1334
|
-
if (
|
|
1538
|
+
if (this.isManagedNativeHandle(handle)) {
|
|
1539
|
+
await this.reapNativePane(handle, now);
|
|
1335
1540
|
continue;
|
|
1336
1541
|
}
|
|
1337
|
-
|
|
1338
|
-
// the session's single event writer, so never reap a session that has one.
|
|
1339
|
-
if (this.liveSessionKeys.has(key)) {
|
|
1542
|
+
if (handle.caps.size > 0 || handle.terminals.size > 0 || handle.live.size > 0) {
|
|
1340
1543
|
continue;
|
|
1341
1544
|
}
|
|
1342
|
-
|
|
1545
|
+
const idleForMs = now - handle.lastUsedAt;
|
|
1546
|
+
if (idleForMs < this.idleTtlMs) {
|
|
1343
1547
|
continue;
|
|
1344
1548
|
}
|
|
1345
|
-
|
|
1549
|
+
const hasActiveResponse = handle.activeResponseIds.size > 0;
|
|
1550
|
+
if (hasActiveResponse)
|
|
1551
|
+
continue;
|
|
1552
|
+
const reason = this.liveSessionKeys.has(key)
|
|
1553
|
+
? "idle live runner reaped"
|
|
1554
|
+
: "idle runner reaped";
|
|
1555
|
+
void this.terminateHandle(handle, reason).catch((error) => {
|
|
1346
1556
|
logTerminationFailure(handle, error);
|
|
1347
1557
|
});
|
|
1348
1558
|
}
|
|
1349
1559
|
}
|
|
1560
|
+
/** Provider-authoritative turn state is the lifecycle truth; attached clients
|
|
1561
|
+
* and tmux's own activity clock are independent busy evidence. There is no
|
|
1562
|
+
* "user was inactive for an hour" override for an active turn. */
|
|
1563
|
+
async reapNativePane(handle, now) {
|
|
1564
|
+
if (this.nativePaneIdleTtlMs <= 0 || handle.dead)
|
|
1565
|
+
return;
|
|
1566
|
+
if (await this.isNativePaneBusy(handle)) {
|
|
1567
|
+
handle.nativePaneLastBusyAt = now;
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const lastBusyAt = handle.nativePaneLastBusyAt;
|
|
1571
|
+
if (lastBusyAt === undefined) {
|
|
1572
|
+
// First idle observation starts a full grace window.
|
|
1573
|
+
handle.nativePaneLastBusyAt = now;
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (now - lastBusyAt < this.nativePaneIdleTtlMs)
|
|
1577
|
+
return;
|
|
1578
|
+
// Close the select→reap race: activity may begin after classification but
|
|
1579
|
+
// before teardown.
|
|
1580
|
+
if (await this.isNativePaneBusy(handle)) {
|
|
1581
|
+
handle.nativePaneLastBusyAt = this.now();
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
// A failed teardown must re-arm on the next scan instead of retrying on
|
|
1585
|
+
// every sweep forever.
|
|
1586
|
+
delete handle.nativePaneLastBusyAt;
|
|
1587
|
+
await this.terminateHandle(handle, "idle native pane reaped").catch((error) => logTerminationFailure(handle, error));
|
|
1588
|
+
}
|
|
1589
|
+
async isNativePaneBusy(handle) {
|
|
1590
|
+
if (handle.activeResponseIds.size > 0 ||
|
|
1591
|
+
handle.caps.size > 0 ||
|
|
1592
|
+
handle.live.size > 0 ||
|
|
1593
|
+
handle.terminals.size > 0) {
|
|
1594
|
+
return true;
|
|
1595
|
+
}
|
|
1596
|
+
const terminalName = `${handle.key}-main`;
|
|
1597
|
+
try {
|
|
1598
|
+
if (await this.terminalHasAttachedClient(terminalName))
|
|
1599
|
+
return true;
|
|
1600
|
+
}
|
|
1601
|
+
catch {
|
|
1602
|
+
// A failed probe contributes no busy evidence; check activity next.
|
|
1603
|
+
}
|
|
1604
|
+
let activityAt = null;
|
|
1605
|
+
try {
|
|
1606
|
+
activityAt = await this.terminalWindowActivityAt(terminalName);
|
|
1607
|
+
}
|
|
1608
|
+
catch {
|
|
1609
|
+
// A failed probe contributes no busy evidence.
|
|
1610
|
+
}
|
|
1611
|
+
return activityAt !== null &&
|
|
1612
|
+
this.wallNow() - activityAt * 1_000 < this.nativePaneOutputBusyWindowMs;
|
|
1613
|
+
}
|
|
1614
|
+
isManagedNativeHandle(handle) {
|
|
1615
|
+
const sessionId = this.currentTerminalSessionId(handle, handle.key);
|
|
1616
|
+
const provider = this.liveOptions.get(sessionId)?.execution.provider;
|
|
1617
|
+
return isManagedNativeProvider(provider);
|
|
1618
|
+
}
|
|
1619
|
+
failActiveResponses(handle, code, message) {
|
|
1620
|
+
const responseIds = [...handle.activeResponseIds];
|
|
1621
|
+
handle.activeResponseIds.clear();
|
|
1622
|
+
const sessionId = this.currentTerminalSessionId(handle, handle.key);
|
|
1623
|
+
for (const responseId of responseIds) {
|
|
1624
|
+
this.mirrorListener?.(sessionId, {
|
|
1625
|
+
type: "response.failed",
|
|
1626
|
+
responseId,
|
|
1627
|
+
error: {
|
|
1628
|
+
source: "execution",
|
|
1629
|
+
code,
|
|
1630
|
+
message,
|
|
1631
|
+
},
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
interruptActiveResponses(handle) {
|
|
1636
|
+
const responseIds = [...handle.activeResponseIds];
|
|
1637
|
+
handle.activeResponseIds.clear();
|
|
1638
|
+
const sessionId = this.currentTerminalSessionId(handle, handle.key);
|
|
1639
|
+
for (const responseId of responseIds) {
|
|
1640
|
+
this.mirrorListener?.(sessionId, {
|
|
1641
|
+
type: "session.interrupted",
|
|
1642
|
+
sessionId,
|
|
1643
|
+
responseId,
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
/** Track the provider-authoritative response lifecycle. Native-pane busy
|
|
1648
|
+
* classification consumes this level directly; output volume is separately
|
|
1649
|
+
* grounded in tmux's own activity clock. */
|
|
1650
|
+
observeHandleRuntimeEvent(handle, event) {
|
|
1651
|
+
switch (event.type) {
|
|
1652
|
+
case "response.created":
|
|
1653
|
+
case "response.output_text.delta":
|
|
1654
|
+
case "response.reasoning_summary_text.delta":
|
|
1655
|
+
case "response.function_call_output.delta":
|
|
1656
|
+
case "response.output_item.done":
|
|
1657
|
+
handle.activeResponseIds.add(event.responseId);
|
|
1658
|
+
return;
|
|
1659
|
+
case "session.interaction.requested":
|
|
1660
|
+
handle.activeResponseIds.add(event.responseId);
|
|
1661
|
+
return;
|
|
1662
|
+
case "response.completed":
|
|
1663
|
+
case "response.failed":
|
|
1664
|
+
case "session.interrupted":
|
|
1665
|
+
handle.activeResponseIds.delete(event.responseId);
|
|
1666
|
+
return;
|
|
1667
|
+
case "session.status":
|
|
1668
|
+
if (event.status === "running" && event.responseId) {
|
|
1669
|
+
handle.activeResponseIds.add(event.responseId);
|
|
1670
|
+
}
|
|
1671
|
+
else if (event.status !== "running") {
|
|
1672
|
+
if (event.responseId)
|
|
1673
|
+
handle.activeResponseIds.delete(event.responseId);
|
|
1674
|
+
else
|
|
1675
|
+
handle.activeResponseIds.clear();
|
|
1676
|
+
}
|
|
1677
|
+
return;
|
|
1678
|
+
case "session.rotated":
|
|
1679
|
+
handle.activeResponseIds.clear();
|
|
1680
|
+
return;
|
|
1681
|
+
default:
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1350
1685
|
openSessionContext(sessionId) {
|
|
1351
1686
|
if (!this.sessionContextProvider)
|
|
1352
1687
|
return undefined;
|
|
@@ -1503,6 +1838,36 @@ function closeSessionContext(context) {
|
|
|
1503
1838
|
}));
|
|
1504
1839
|
}
|
|
1505
1840
|
}
|
|
1841
|
+
function isDetachedGeneratedImageEvent(event) {
|
|
1842
|
+
if (event.type !== "response.output_item.done" ||
|
|
1843
|
+
event.item.type !== "function_call_output") {
|
|
1844
|
+
return false;
|
|
1845
|
+
}
|
|
1846
|
+
const data = event.item.data;
|
|
1847
|
+
return Boolean(data.__rynxGeneratedImage && data.__rynxGeneratedImage.result === undefined);
|
|
1848
|
+
}
|
|
1849
|
+
function attachGeneratedImageResult(event, result) {
|
|
1850
|
+
if (!isDetachedGeneratedImageEvent(event))
|
|
1851
|
+
return undefined;
|
|
1852
|
+
const outputEvent = event;
|
|
1853
|
+
const item = outputEvent.item;
|
|
1854
|
+
if (item.type !== "function_call_output")
|
|
1855
|
+
return undefined;
|
|
1856
|
+
const data = item.data;
|
|
1857
|
+
return {
|
|
1858
|
+
...outputEvent,
|
|
1859
|
+
item: {
|
|
1860
|
+
...item,
|
|
1861
|
+
data: {
|
|
1862
|
+
...data,
|
|
1863
|
+
__rynxGeneratedImage: {
|
|
1864
|
+
...data.__rynxGeneratedImage,
|
|
1865
|
+
result,
|
|
1866
|
+
},
|
|
1867
|
+
},
|
|
1868
|
+
},
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1506
1871
|
function errorMessage(error) {
|
|
1507
1872
|
return error instanceof Error ? error.message : String(error);
|
|
1508
1873
|
}
|