@rynx-ai/runtime 0.1.11-beta.22 → 0.1.11-beta.24
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/native-integration.d.ts +7 -0
- package/dist/claude/native-integration.js +25 -2
- package/dist/codex-app-server/forwarder.d.ts +2 -0
- package/dist/codex-app-server/forwarder.js +12 -2
- package/dist/codex-app-server/mapping.d.ts +1 -0
- package/dist/codex-app-server/mapping.js +8 -1
- package/dist/host.d.ts +9 -4
- package/dist/host.js +262 -95
- package/dist/index.d.ts +1 -1
- package/dist/runner/child.d.ts +6 -2
- package/dist/runner/child.js +59 -14
- package/dist/runner/manager.d.ts +6 -9
- package/dist/runner/manager.js +29 -50
- package/dist/runner/protocol.d.ts +6 -6
- package/dist/terminal/registry.js +3 -2
- package/dist/terminal/tmux.d.ts +29 -5
- package/dist/terminal/tmux.js +98 -52
- package/package.json +2 -2
package/dist/host.js
CHANGED
|
@@ -9,7 +9,7 @@ import { getRuntimeProfile, } from "@rynx-ai/core";
|
|
|
9
9
|
import { resolveRuntimeBinary, } from "@rynx-ai/core";
|
|
10
10
|
import { createCodexChildEnv } from "./codex-child-env.js";
|
|
11
11
|
import { prepareRuntimeHome, populateCodexSkills, runtimeHomePath, } from "./codex-home.js";
|
|
12
|
-
import {
|
|
12
|
+
import { materializeClaudePlugin, reuseClaudePlugin, } from "./claude/executor.js";
|
|
13
13
|
import { listClaudeModels } from "./claude/models.js";
|
|
14
14
|
import { CodexAppServerClient, buildRuntimeUserInput, } from "./codex-app-server/client.js";
|
|
15
15
|
import { buildAppServerBaseArgs, CodexTransportError, } from "./codex-app-server/transport.js";
|
|
@@ -529,16 +529,21 @@ export class LocalAgentHost {
|
|
|
529
529
|
return sameSessionSnapshots(existingClaude.workspace, existingClaude.execution, snapshotOpts);
|
|
530
530
|
}
|
|
531
531
|
// Dedupe concurrent triggers so only one forwarder is ever created per
|
|
532
|
-
// session (two would double-mirror every turn).
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
// the thread id instead of launching a bare `codex --remote` pane.
|
|
532
|
+
// session (two would double-mirror every turn). startLiveCodexSession
|
|
533
|
+
// publishes its partially initialized session before thread/start completes,
|
|
534
|
+
// so wait for that startup and then adopt any newer mutable Turn settings.
|
|
536
535
|
const inflight = this.liveEnsuring.get(localThreadId);
|
|
537
|
-
if (inflight)
|
|
538
|
-
|
|
536
|
+
if (inflight) {
|
|
537
|
+
if (!await inflight)
|
|
538
|
+
return false;
|
|
539
|
+
const startedLive = this.liveSessions.get(localThreadId);
|
|
540
|
+
return startedLive
|
|
541
|
+
? updateLiveCodexTurnSettings(startedLive, snapshotOpts)
|
|
542
|
+
: false;
|
|
543
|
+
}
|
|
539
544
|
const live = this.liveSessions.get(localThreadId);
|
|
540
545
|
if (live)
|
|
541
|
-
return
|
|
546
|
+
return updateLiveCodexTurnSettings(live, snapshotOpts);
|
|
542
547
|
const started = this.startLiveCodexSession(localThreadId, emit, snapshotOpts);
|
|
543
548
|
this.liveEnsuring.set(localThreadId, started);
|
|
544
549
|
try {
|
|
@@ -670,6 +675,8 @@ export class LocalAgentHost {
|
|
|
670
675
|
approvalPolicy,
|
|
671
676
|
model,
|
|
672
677
|
reasoningEffort,
|
|
678
|
+
appliedModel: model,
|
|
679
|
+
appliedReasoningEffort: reasoningEffort,
|
|
673
680
|
...(execution.instructions ? { instructions: execution.instructions } : {}),
|
|
674
681
|
threadId: record?.codexSessionId ?? null,
|
|
675
682
|
ready,
|
|
@@ -681,6 +688,10 @@ export class LocalAgentHost {
|
|
|
681
688
|
injectLock: Promise.resolve(),
|
|
682
689
|
pendingInjectedInputs: [],
|
|
683
690
|
publishInjectedInput: () => undefined,
|
|
691
|
+
publishTurnAdmission: () => undefined,
|
|
692
|
+
publishTurnAdmissionFailure: () => undefined,
|
|
693
|
+
publishInterrupted: () => undefined,
|
|
694
|
+
interruptedResponseId: null,
|
|
684
695
|
subscribing: false,
|
|
685
696
|
rotationPending: false,
|
|
686
697
|
stopped: false,
|
|
@@ -698,11 +709,20 @@ export class LocalAgentHost {
|
|
|
698
709
|
}
|
|
699
710
|
emit(event);
|
|
700
711
|
};
|
|
712
|
+
live.publishInterrupted = (responseId) => {
|
|
713
|
+
if (live.interruptedResponseId === responseId)
|
|
714
|
+
return;
|
|
715
|
+
live.interruptedResponseId = responseId;
|
|
716
|
+
emitCurrent({
|
|
717
|
+
type: "session.interrupted",
|
|
718
|
+
sessionId: currentSessionId,
|
|
719
|
+
responseId,
|
|
720
|
+
});
|
|
721
|
+
};
|
|
701
722
|
let normalizer = null;
|
|
702
723
|
let currentResponseId = null;
|
|
703
724
|
const startNormalizer = (turnId) => {
|
|
704
|
-
const responseId =
|
|
705
|
-
(turnId ? `resp_codex_${turnId}` : "resp_codex_native");
|
|
725
|
+
const responseId = turnId ? `resp_codex_${turnId}` : "resp_codex_native";
|
|
706
726
|
if (normalizer && currentResponseId === responseId)
|
|
707
727
|
return normalizer;
|
|
708
728
|
currentResponseId = responseId;
|
|
@@ -726,6 +746,21 @@ export class LocalAgentHost {
|
|
|
726
746
|
for (const se of n.userInput(content))
|
|
727
747
|
emitCurrent(se);
|
|
728
748
|
};
|
|
749
|
+
live.publishTurnAdmission = () => {
|
|
750
|
+
emitCurrent({
|
|
751
|
+
type: "session.status",
|
|
752
|
+
sessionId: currentSessionId,
|
|
753
|
+
status: "running",
|
|
754
|
+
});
|
|
755
|
+
};
|
|
756
|
+
live.publishTurnAdmissionFailure = (message) => {
|
|
757
|
+
emitCurrent({
|
|
758
|
+
type: "session.status",
|
|
759
|
+
sessionId: currentSessionId,
|
|
760
|
+
status: "failed",
|
|
761
|
+
note: message,
|
|
762
|
+
});
|
|
763
|
+
};
|
|
729
764
|
const clearPendingInputsForResponse = (responseId) => {
|
|
730
765
|
if (!responseId)
|
|
731
766
|
return;
|
|
@@ -842,6 +877,22 @@ export class LocalAgentHost {
|
|
|
842
877
|
normalizer = null;
|
|
843
878
|
currentResponseId = null;
|
|
844
879
|
};
|
|
880
|
+
const interruptCurrentTurn = () => {
|
|
881
|
+
closeCanonicalInteractions();
|
|
882
|
+
if (!normalizer)
|
|
883
|
+
return;
|
|
884
|
+
const interruptedResponseId = currentResponseId;
|
|
885
|
+
for (const se of normalizer.interrupt()) {
|
|
886
|
+
if (se.type === "session.interrupted" &&
|
|
887
|
+
live.interruptedResponseId === interruptedResponseId)
|
|
888
|
+
continue;
|
|
889
|
+
emitCurrent(se);
|
|
890
|
+
}
|
|
891
|
+
live.interruptedResponseId = null;
|
|
892
|
+
clearPendingInputsForResponse(interruptedResponseId);
|
|
893
|
+
normalizer = null;
|
|
894
|
+
currentResponseId = null;
|
|
895
|
+
};
|
|
845
896
|
const sink = {
|
|
846
897
|
onTurnStart: (turnId) => startNormalizer(turnId),
|
|
847
898
|
onTurnObserved: (turnId) => {
|
|
@@ -856,7 +907,7 @@ export class LocalAgentHost {
|
|
|
856
907
|
: content;
|
|
857
908
|
const signature = JSON.stringify(normalizedContent);
|
|
858
909
|
const pending = live.pendingInjectedInputs.find((entry) => entry.signature === signature);
|
|
859
|
-
if (pending?.state === "optimistic"
|
|
910
|
+
if (pending?.state === "optimistic") {
|
|
860
911
|
live.pendingInjectedInputs.splice(live.pendingInjectedInputs.indexOf(pending), 1);
|
|
861
912
|
return;
|
|
862
913
|
}
|
|
@@ -887,6 +938,7 @@ export class LocalAgentHost {
|
|
|
887
938
|
// response without publishing a false idle edge in a running→running
|
|
888
939
|
// transition.
|
|
889
940
|
onTurnEnd: completeCurrentTurn,
|
|
941
|
+
onTurnInterrupted: interruptCurrentTurn,
|
|
890
942
|
onRecoveredTurnStatus: (status, turnId, error) => {
|
|
891
943
|
const responseId = turnId ? `resp_codex_${turnId}` : undefined;
|
|
892
944
|
if (normalizer && (!responseId || currentResponseId === responseId)) {
|
|
@@ -1132,12 +1184,7 @@ export class LocalAgentHost {
|
|
|
1132
1184
|
return;
|
|
1133
1185
|
live.startupError = error;
|
|
1134
1186
|
this.liveStartupErrors.set(localThreadId, error);
|
|
1135
|
-
|
|
1136
|
-
const appServerOwner = live.appServerOwner;
|
|
1137
|
-
this.stopLiveCodexSession(localThreadId);
|
|
1138
|
-
void appServerOwner.stop().catch((stopError) => {
|
|
1139
|
-
console.warn(`[codex-live] session=${localThreadId} runtime=${live.runtime} app-server cleanup after observer failure failed: ${stopError instanceof Error ? stopError.message : String(stopError)}`);
|
|
1140
|
-
});
|
|
1187
|
+
this.teardownLiveCodexSession(localThreadId, error);
|
|
1141
1188
|
}
|
|
1142
1189
|
shouldIgnoreManagedForkThreadStarted(live, threadId, forkedFromId) {
|
|
1143
1190
|
const pending = live.managedFork;
|
|
@@ -1292,7 +1339,7 @@ export class LocalAgentHost {
|
|
|
1292
1339
|
* resumed from the persisted native id. Serialized per session so two
|
|
1293
1340
|
* injects can't double-open a turn.
|
|
1294
1341
|
*
|
|
1295
|
-
* Returns an {@link
|
|
1342
|
+
* Returns an {@link InjectResult}: `notLive` when this session has no live
|
|
1296
1343
|
* forwarder (caller may use the run path); `notReady`/`failed` are hard errors
|
|
1297
1344
|
* the caller reports WITHOUT re-running (re-running double-writes alongside the
|
|
1298
1345
|
* forwarder). Park-until-ready (~60s), aligning reference implementation's executor waiting for
|
|
@@ -1309,9 +1356,8 @@ export class LocalAgentHost {
|
|
|
1309
1356
|
const pendingInput = {
|
|
1310
1357
|
content,
|
|
1311
1358
|
signature: JSON.stringify(content),
|
|
1312
|
-
state:
|
|
1359
|
+
state: "awaiting",
|
|
1313
1360
|
observed: false,
|
|
1314
|
-
...(runtimeInput.responseId ? { responseId: runtimeInput.responseId } : {}),
|
|
1315
1361
|
};
|
|
1316
1362
|
claude.pendingInjectedInputs.push(pendingInput);
|
|
1317
1363
|
const forgetPendingInput = () => {
|
|
@@ -1325,8 +1371,17 @@ export class LocalAgentHost {
|
|
|
1325
1371
|
const expiry = setTimeout(() => claude.pendingImageInputs.delete(token), 5 * 60_000);
|
|
1326
1372
|
expiry.unref?.();
|
|
1327
1373
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1374
|
+
let result;
|
|
1375
|
+
try {
|
|
1376
|
+
result = await this.injectClaude(claude, localThreadId, text, pendingInput);
|
|
1377
|
+
}
|
|
1378
|
+
catch (error) {
|
|
1379
|
+
forgetPendingInput();
|
|
1380
|
+
if (token)
|
|
1381
|
+
claude.pendingImageInputs.delete(token);
|
|
1382
|
+
throw error;
|
|
1383
|
+
}
|
|
1384
|
+
if (result.outcome !== "injected" && result.outcome !== "steered") {
|
|
1330
1385
|
forgetPendingInput();
|
|
1331
1386
|
if (token)
|
|
1332
1387
|
claude.pendingImageInputs.delete(token);
|
|
@@ -1334,31 +1389,37 @@ export class LocalAgentHost {
|
|
|
1334
1389
|
else if (pendingInput.observed) {
|
|
1335
1390
|
forgetPendingInput();
|
|
1336
1391
|
}
|
|
1337
|
-
return
|
|
1392
|
+
return result;
|
|
1338
1393
|
}
|
|
1339
1394
|
const live = this.liveSessions.get(localThreadId);
|
|
1340
1395
|
if (!live)
|
|
1341
|
-
return "notLive";
|
|
1396
|
+
return { outcome: "notLive" };
|
|
1342
1397
|
const run = live.injectLock.then(async () => {
|
|
1343
1398
|
if (live.rotationPending || live.stopped)
|
|
1344
|
-
return "failed";
|
|
1399
|
+
return { outcome: "failed" };
|
|
1345
1400
|
// Park until the thread binds (~60s, reference implementation codex_native_executor:177-186),
|
|
1346
1401
|
// not a 20s race that returns false and lets the caller re-run on a 2nd path.
|
|
1347
1402
|
const bound = await this.waitLiveReady(localThreadId, CODEX_BRIDGE_READY_TIMEOUT_MS);
|
|
1348
1403
|
const threadId = live.threadId ?? live.forwarder.threadId();
|
|
1349
1404
|
if (!bound || !threadId)
|
|
1350
|
-
return "notReady";
|
|
1405
|
+
return { outcome: "notReady" };
|
|
1351
1406
|
const nativeInput = buildRuntimeUserInput(runtimeInput);
|
|
1352
1407
|
const content = runtimeUserContent(runtimeInput);
|
|
1353
1408
|
const pendingInput = {
|
|
1354
1409
|
content,
|
|
1355
1410
|
signature: JSON.stringify(content),
|
|
1356
|
-
state:
|
|
1411
|
+
state: "awaiting",
|
|
1357
1412
|
observed: false,
|
|
1358
|
-
...(runtimeInput.responseId ? { responseId: runtimeInput.responseId } : {}),
|
|
1359
1413
|
};
|
|
1360
1414
|
live.pendingInjectedInputs.push(pendingInput);
|
|
1361
1415
|
let injectionMethod = "turn/start";
|
|
1416
|
+
let admissionPublished = false;
|
|
1417
|
+
const publishAdmission = () => {
|
|
1418
|
+
if (admissionPublished)
|
|
1419
|
+
return;
|
|
1420
|
+
admissionPublished = true;
|
|
1421
|
+
live.publishTurnAdmission();
|
|
1422
|
+
};
|
|
1362
1423
|
const forgetPendingInput = () => {
|
|
1363
1424
|
const index = live.pendingInjectedInputs.indexOf(pendingInput);
|
|
1364
1425
|
if (index >= 0)
|
|
@@ -1366,6 +1427,12 @@ export class LocalAgentHost {
|
|
|
1366
1427
|
};
|
|
1367
1428
|
const injectionClient = this.injectionClientFactory(live.appServerUrl);
|
|
1368
1429
|
try {
|
|
1430
|
+
// Omnigent publishes codex-native running only after the native Terminal
|
|
1431
|
+
// is ready and the runner has accepted the message, but before the
|
|
1432
|
+
// short-lived app-server client initializes and starts the Turn. Claude
|
|
1433
|
+
// deliberately has no matching synthesized edge.
|
|
1434
|
+
if (!live.forwarder.isTurnOpen())
|
|
1435
|
+
publishAdmission();
|
|
1369
1436
|
await injectionClient.ensureInitialized();
|
|
1370
1437
|
// Match Omnigent's executor: each message uses one initialized client
|
|
1371
1438
|
// that closes as soon as turn/start or turn/steer is acknowledged.
|
|
@@ -1378,11 +1445,7 @@ export class LocalAgentHost {
|
|
|
1378
1445
|
expectedTurnId: turnId,
|
|
1379
1446
|
input: nativeInput,
|
|
1380
1447
|
});
|
|
1381
|
-
if (pendingInput.
|
|
1382
|
-
// The caller already persisted and published this user input
|
|
1383
|
-
// before waiting for the native Terminal to become ready.
|
|
1384
|
-
}
|
|
1385
|
-
else if (pendingInput.observed) {
|
|
1448
|
+
if (pendingInput.observed) {
|
|
1386
1449
|
forgetPendingInput();
|
|
1387
1450
|
}
|
|
1388
1451
|
else {
|
|
@@ -1390,24 +1453,39 @@ export class LocalAgentHost {
|
|
|
1390
1453
|
pendingInput.state = "optimistic";
|
|
1391
1454
|
}
|
|
1392
1455
|
live.forwarder.noteTurnAccepted(steered.turnId);
|
|
1393
|
-
return "steered";
|
|
1456
|
+
return { outcome: "steered", responseId: `resp_codex_${steered.turnId}` };
|
|
1394
1457
|
}
|
|
1395
1458
|
}
|
|
1396
|
-
//
|
|
1397
|
-
//
|
|
1459
|
+
// Match Omnigent's turn boundary: change the native thread settings
|
|
1460
|
+
// under the same lock immediately before starting the next Turn. Never
|
|
1461
|
+
// put settings on turn/start or mutate a Turn that is already open.
|
|
1462
|
+
const desiredModel = live.model;
|
|
1463
|
+
const desiredReasoningEffort = live.reasoningEffort;
|
|
1464
|
+
const settings = {
|
|
1465
|
+
threadId,
|
|
1466
|
+
...(desiredModel !== live.appliedModel
|
|
1467
|
+
? { model: desiredModel || null }
|
|
1468
|
+
: {}),
|
|
1469
|
+
...(desiredReasoningEffort !== live.appliedReasoningEffort
|
|
1470
|
+
? { effort: desiredReasoningEffort ?? null }
|
|
1471
|
+
: {}),
|
|
1472
|
+
};
|
|
1473
|
+
if (Object.keys(settings).length > 1) {
|
|
1474
|
+
await injectionClient.threadSettingsUpdate(settings);
|
|
1475
|
+
// Record exactly what this RPC carried. A concurrent settings save may
|
|
1476
|
+
// already have advanced the desired fields and must remain pending for
|
|
1477
|
+
// the following new Turn.
|
|
1478
|
+
live.appliedModel = desiredModel;
|
|
1479
|
+
live.appliedReasoningEffort = desiredReasoningEffort;
|
|
1480
|
+
}
|
|
1481
|
+
publishAdmission();
|
|
1398
1482
|
const started = await injectionClient.turnStart({
|
|
1399
1483
|
threadId,
|
|
1400
1484
|
input: nativeInput,
|
|
1401
1485
|
...turnWorkspaceParams(live.runtime, live.workspace, live.sandbox),
|
|
1402
1486
|
approvalPolicy: live.approvalPolicy,
|
|
1403
|
-
...(live.model ? { model: live.model } : {}),
|
|
1404
|
-
...(live.reasoningEffort ? { effort: live.reasoningEffort } : {}),
|
|
1405
1487
|
});
|
|
1406
|
-
if (pendingInput.
|
|
1407
|
-
// The caller already persisted and published this user input before
|
|
1408
|
-
// waiting for the native Terminal to become ready.
|
|
1409
|
-
}
|
|
1410
|
-
else if (pendingInput.observed) {
|
|
1488
|
+
if (pendingInput.observed) {
|
|
1411
1489
|
forgetPendingInput();
|
|
1412
1490
|
}
|
|
1413
1491
|
else {
|
|
@@ -1418,7 +1496,7 @@ export class LocalAgentHost {
|
|
|
1418
1496
|
// Do not wait for the independent observer connection's `turn/started`:
|
|
1419
1497
|
// a second message accepted in that window must steer, not double-start.
|
|
1420
1498
|
live.forwarder.noteTurnAccepted(started.turnId);
|
|
1421
|
-
return "injected";
|
|
1499
|
+
return { outcome: "injected", responseId: `resp_codex_${started.turnId}` };
|
|
1422
1500
|
}
|
|
1423
1501
|
catch (error) {
|
|
1424
1502
|
forgetPendingInput();
|
|
@@ -1427,6 +1505,13 @@ export class LocalAgentHost {
|
|
|
1427
1505
|
const baseDetail = codexRpcError(error, injectionMethod);
|
|
1428
1506
|
const startupDetail = live.forwarder.mcpStartupDetail();
|
|
1429
1507
|
const detail = startupDetail ? `${baseDetail} (${startupDetail})` : baseDetail;
|
|
1508
|
+
// A turn may open on the observer while this short-lived client is
|
|
1509
|
+
// initializing, converting the submission to steer. A rejected steer
|
|
1510
|
+
// does not fail that already-running Turn; only a new-Turn admission
|
|
1511
|
+
// owns the terminal failed edge.
|
|
1512
|
+
if (admissionPublished && injectionMethod === "turn/start") {
|
|
1513
|
+
live.publishTurnAdmissionFailure(detail);
|
|
1514
|
+
}
|
|
1430
1515
|
console.error(`[codex-live] session=${localThreadId} runtime=${live.runtime} injection failed: ${detail}`);
|
|
1431
1516
|
throw nativeLiveFailure(live.runtime, "native_message_injection_failed", `message injection via ${injectionMethod} failed`, detail);
|
|
1432
1517
|
}
|
|
@@ -1501,6 +1586,7 @@ export class LocalAgentHost {
|
|
|
1501
1586
|
if (turnId) {
|
|
1502
1587
|
try {
|
|
1503
1588
|
await interruptClient.turnInterrupt({ threadId, turnId });
|
|
1589
|
+
live.publishInterrupted(`resp_codex_${turnId}`);
|
|
1504
1590
|
handled = true;
|
|
1505
1591
|
}
|
|
1506
1592
|
catch {
|
|
@@ -1528,8 +1614,6 @@ export class LocalAgentHost {
|
|
|
1528
1614
|
claude.forwarder.finalizeStop();
|
|
1529
1615
|
}
|
|
1530
1616
|
removeManagedClaudeSettings(claude.bridgeDir);
|
|
1531
|
-
// Remove the throwaway skills --plugin-dir temp so it doesn't leak per launch.
|
|
1532
|
-
void claude.skillCleanup?.();
|
|
1533
1617
|
return;
|
|
1534
1618
|
}
|
|
1535
1619
|
const live = this.liveSessions.get(localThreadId);
|
|
@@ -1555,6 +1639,27 @@ export class LocalAgentHost {
|
|
|
1555
1639
|
// Remove the session-scoped skills dir — the machine keeps zero task residue.
|
|
1556
1640
|
void live.skillsCleanup?.();
|
|
1557
1641
|
}
|
|
1642
|
+
/** Tear down one codex-lineage native runtime without deleting its durable
|
|
1643
|
+
* session-store binding. Omnigent couples its auxiliary Terminal, observer,
|
|
1644
|
+
* forwarder and per-session app-server as one disposable runtime envelope;
|
|
1645
|
+
* the next message recreates that envelope and cold-resumes the native id. */
|
|
1646
|
+
teardownLiveCodexSession(localThreadId, error) {
|
|
1647
|
+
const live = this.liveSessions.get(localThreadId);
|
|
1648
|
+
if (!live)
|
|
1649
|
+
return false;
|
|
1650
|
+
const turnFailed = error ? live.forwarder.failOpenTurn(error) : false;
|
|
1651
|
+
const backendKey = codexBackendKey(live.runtime, live.execution.budget ?? undefined);
|
|
1652
|
+
const backend = this.backends.get(backendKey);
|
|
1653
|
+
const appServerOwner = live.appServerOwner;
|
|
1654
|
+
this.stopLiveCodexSession(localThreadId);
|
|
1655
|
+
if (backend?.appServerClient === appServerOwner) {
|
|
1656
|
+
this.backends.delete(backendKey);
|
|
1657
|
+
}
|
|
1658
|
+
void appServerOwner.stop().catch((stopError) => {
|
|
1659
|
+
console.warn(`[codex-live] session=${localThreadId} runtime=${live.runtime} app-server teardown failed: ${stopError instanceof Error ? stopError.message : String(stopError)}`);
|
|
1660
|
+
});
|
|
1661
|
+
return turnFailed;
|
|
1662
|
+
}
|
|
1558
1663
|
/** Complete the second shutdown phase after the runner has killed all native
|
|
1559
1664
|
* terminals and hook subprocesses. Must run before the runner process exits. */
|
|
1560
1665
|
finalizeStoppedLiveSessions() {
|
|
@@ -1745,17 +1850,27 @@ export class LocalAgentHost {
|
|
|
1745
1850
|
throw new CodexRuntimeError(`cannot launch Claude from ${execution.provider} execution snapshot`, 422, "invalid_execution_snapshot");
|
|
1746
1851
|
}
|
|
1747
1852
|
const cwd = workspace.cwd;
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1853
|
+
const hasSkillSnapshot = execution.skills.length > 0 || execution.pluginSkills.length > 0;
|
|
1854
|
+
let skillPlugin = hasSkillSnapshot
|
|
1855
|
+
? await reuseClaudePlugin(localThreadId)
|
|
1856
|
+
: null;
|
|
1857
|
+
if (!skillPlugin && hasSkillSnapshot) {
|
|
1858
|
+
let snapshotSkills;
|
|
1859
|
+
try {
|
|
1860
|
+
snapshotSkills = await this.prepareExecutionSkills(execution);
|
|
1861
|
+
}
|
|
1862
|
+
catch (err) {
|
|
1863
|
+
console.error(`[session-snapshot] session=${localThreadId} skill materialization failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
try {
|
|
1867
|
+
skillPlugin = await materializeClaudePlugin(localThreadId, snapshotSkills.selectedSkills);
|
|
1868
|
+
}
|
|
1869
|
+
finally {
|
|
1870
|
+
await snapshotSkills.skillsCleanup();
|
|
1871
|
+
}
|
|
1755
1872
|
}
|
|
1756
1873
|
const model = execution.model ?? "";
|
|
1757
|
-
const skillPlugin = await materializeSkillPlugin(snapshotSkills.selectedSkills);
|
|
1758
|
-
void snapshotSkills.skillsCleanup();
|
|
1759
1874
|
const permissionMode = execution.permissionMode;
|
|
1760
1875
|
const launchExtraArgs = [
|
|
1761
1876
|
...(skillPlugin?.pluginDir ? ["--plugin-dir", skillPlugin.pluginDir] : []),
|
|
@@ -1776,8 +1891,7 @@ export class LocalAgentHost {
|
|
|
1776
1891
|
};
|
|
1777
1892
|
const startNormalizer = (turnId) => {
|
|
1778
1893
|
// turnId unknown → fixed literal (never random), aligning reference implementation `_response_id`.
|
|
1779
|
-
const responseId =
|
|
1780
|
-
(turnId ? `resp_claude_${turnId}` : "resp_claude_native");
|
|
1894
|
+
const responseId = turnId ? `resp_claude_${turnId}` : "resp_claude_native";
|
|
1781
1895
|
if (normalizer && currentResponseId === responseId)
|
|
1782
1896
|
return normalizer;
|
|
1783
1897
|
currentResponseId = responseId;
|
|
@@ -1822,6 +1936,8 @@ export class LocalAgentHost {
|
|
|
1822
1936
|
injectLock: Promise.resolve(),
|
|
1823
1937
|
pendingImageInputs: new Map(),
|
|
1824
1938
|
pendingInjectedInputs: [],
|
|
1939
|
+
currentResponseId: () => currentResponseId,
|
|
1940
|
+
publishInterrupted: () => undefined,
|
|
1825
1941
|
ready,
|
|
1826
1942
|
markReady,
|
|
1827
1943
|
failed,
|
|
@@ -1831,7 +1947,55 @@ export class LocalAgentHost {
|
|
|
1831
1947
|
launchExtraArgs,
|
|
1832
1948
|
permissionMode,
|
|
1833
1949
|
...(forkIntent ? { forkIntent } : {}),
|
|
1834
|
-
|
|
1950
|
+
};
|
|
1951
|
+
live.publishInterrupted = (responseId) => {
|
|
1952
|
+
if (live.interruptedResponseId === responseId)
|
|
1953
|
+
return;
|
|
1954
|
+
live.interruptedResponseId = responseId;
|
|
1955
|
+
emitCurrent({
|
|
1956
|
+
type: "session.interrupted",
|
|
1957
|
+
sessionId: currentSessionId,
|
|
1958
|
+
responseId,
|
|
1959
|
+
});
|
|
1960
|
+
};
|
|
1961
|
+
const settleClaudeTurn = (interrupted, usage) => {
|
|
1962
|
+
if (!normalizer)
|
|
1963
|
+
return;
|
|
1964
|
+
const rid = currentResponseId;
|
|
1965
|
+
if (interrupted) {
|
|
1966
|
+
for (const se of normalizer.interrupt()) {
|
|
1967
|
+
if (se.type === "session.interrupted" &&
|
|
1968
|
+
live.interruptedResponseId === rid)
|
|
1969
|
+
continue;
|
|
1970
|
+
emitCurrent(se);
|
|
1971
|
+
}
|
|
1972
|
+
live.interruptedResponseId = undefined;
|
|
1973
|
+
}
|
|
1974
|
+
else {
|
|
1975
|
+
// statusLine usage (context/cost) rides the turn's response.completed.
|
|
1976
|
+
if (usage) {
|
|
1977
|
+
for (const se of normalizer.next({ type: "turn_completed", usage }))
|
|
1978
|
+
emitCurrent(se);
|
|
1979
|
+
}
|
|
1980
|
+
for (const se of normalizer.next({ type: "done" }))
|
|
1981
|
+
emitCurrent(se);
|
|
1982
|
+
}
|
|
1983
|
+
live.pendingInjectedInputs = live.pendingInjectedInputs.filter((entry) => entry.responseId !== rid);
|
|
1984
|
+
normalizer = null;
|
|
1985
|
+
currentResponseId = undefined;
|
|
1986
|
+
// One-time context-window banner past CONTEXT_WARN_RATIO (a transient note,
|
|
1987
|
+
// cleared by the next turn's first item). statusLine pre-computes the %.
|
|
1988
|
+
const pct = usage && typeof usage.used_percentage === "number" ? usage.used_percentage : undefined;
|
|
1989
|
+
if (!interrupted && rid && pct !== undefined && !live.contextWarned && pct >= CONTEXT_WARN_RATIO * 100) {
|
|
1990
|
+
live.contextWarned = true;
|
|
1991
|
+
emitCurrent({
|
|
1992
|
+
type: "session.status",
|
|
1993
|
+
sessionId: currentSessionId,
|
|
1994
|
+
responseId: rid,
|
|
1995
|
+
status: "idle",
|
|
1996
|
+
note: `context ${Math.round(pct)}% full — consider /compact`,
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1835
1999
|
};
|
|
1836
2000
|
const sink = {
|
|
1837
2001
|
onTurnStart: (turnId) => startNormalizer(turnId),
|
|
@@ -1844,12 +2008,13 @@ export class LocalAgentHost {
|
|
|
1844
2008
|
const normalizedContent = content ?? [{ type: "input_text", text }];
|
|
1845
2009
|
const signature = JSON.stringify(normalizedContent);
|
|
1846
2010
|
const pending = live.pendingInjectedInputs.find((entry) => entry.signature === signature);
|
|
1847
|
-
if (pending
|
|
2011
|
+
if (pending) {
|
|
2012
|
+
pending.responseId = currentResponseId;
|
|
1848
2013
|
live.pendingInjectedInputs.splice(live.pendingInjectedInputs.indexOf(pending), 1);
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
if (pending)
|
|
2014
|
+
if (pending.state === "optimistic")
|
|
2015
|
+
return;
|
|
1852
2016
|
pending.observed = true;
|
|
2017
|
+
}
|
|
1853
2018
|
for (const se of n.userInput(normalizedContent))
|
|
1854
2019
|
emitCurrent(se);
|
|
1855
2020
|
},
|
|
@@ -1876,33 +2041,12 @@ export class LocalAgentHost {
|
|
|
1876
2041
|
...(blockedOn ? { note: blockedOn } : {}),
|
|
1877
2042
|
});
|
|
1878
2043
|
},
|
|
1879
|
-
onTurnEnd: (usage) =>
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
const
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
for (const se of normalizer.next({ type: "turn_completed", usage }))
|
|
1886
|
-
emitCurrent(se);
|
|
1887
|
-
}
|
|
1888
|
-
for (const se of normalizer.next({ type: "done" }))
|
|
1889
|
-
emitCurrent(se);
|
|
1890
|
-
live.pendingInjectedInputs = live.pendingInjectedInputs.filter((entry) => entry.responseId !== rid);
|
|
1891
|
-
normalizer = null;
|
|
1892
|
-
currentResponseId = undefined;
|
|
1893
|
-
// One-time context-window banner past CONTEXT_WARN_RATIO (a transient note,
|
|
1894
|
-
// cleared by the next turn's first item). statusLine pre-computes the %.
|
|
1895
|
-
const pct = usage && typeof usage.used_percentage === "number" ? usage.used_percentage : undefined;
|
|
1896
|
-
if (rid && pct !== undefined && !live.contextWarned && pct >= CONTEXT_WARN_RATIO * 100) {
|
|
1897
|
-
live.contextWarned = true;
|
|
1898
|
-
emitCurrent({
|
|
1899
|
-
type: "session.status",
|
|
1900
|
-
sessionId: currentSessionId,
|
|
1901
|
-
responseId: rid,
|
|
1902
|
-
status: "idle",
|
|
1903
|
-
note: `context ${Math.round(pct)}% full — consider /compact`,
|
|
1904
|
-
});
|
|
1905
|
-
}
|
|
2044
|
+
onTurnEnd: (usage) => settleClaudeTurn(false, usage),
|
|
2045
|
+
onTurnInterrupted: (usage) => settleClaudeTurn(true, usage),
|
|
2046
|
+
onTurnInterruptRequested: () => {
|
|
2047
|
+
const responseId = currentResponseId;
|
|
2048
|
+
if (responseId)
|
|
2049
|
+
live.publishInterrupted(responseId);
|
|
1906
2050
|
},
|
|
1907
2051
|
onIdle: () => {
|
|
1908
2052
|
// Surface idle on the current turn WITHOUT finalizing it (see the sink's
|
|
@@ -2037,17 +2181,18 @@ export class LocalAgentHost {
|
|
|
2037
2181
|
* serialized per session. Parks until the thread is ready AND the tmux injector
|
|
2038
2182
|
* is (re)attached, then pastes; `injectViaTerminal` RAISES if the prompt never
|
|
2039
2183
|
* appears (reference implementation RAISE), so a not-ready pane is a hard error — NOT a
|
|
2040
|
-
* fall-through-to-run signal. Returns {@link
|
|
2041
|
-
injectClaude(live, localThreadId, text) {
|
|
2184
|
+
* fall-through-to-run signal. Returns {@link InjectResult}. */
|
|
2185
|
+
injectClaude(live, localThreadId, text, pendingInput) {
|
|
2042
2186
|
const run = live.injectLock.then(async () => {
|
|
2043
2187
|
const ready = await this.waitLiveReady(localThreadId, 60_000);
|
|
2044
2188
|
if (!ready)
|
|
2045
|
-
return "notReady";
|
|
2189
|
+
return { outcome: "notReady" };
|
|
2046
2190
|
// Pane may have just relaunched — park until its injector re-attaches
|
|
2047
2191
|
// (attachTerminalInjector resets it) instead of returning false → fallback.
|
|
2048
2192
|
const injector = await this.waitInjector(live, 60_000);
|
|
2049
2193
|
if (!injector)
|
|
2050
|
-
return "notReady";
|
|
2194
|
+
return { outcome: "notReady" };
|
|
2195
|
+
const steered = live.forwarder.isTurnOpen();
|
|
2051
2196
|
// Abortable: the web Stop button cancels an in-flight paste/submit (before
|
|
2052
2197
|
// the message reaches claude) via interruptLive → injectAbort.abort().
|
|
2053
2198
|
const abort = new AbortController();
|
|
@@ -2058,11 +2203,18 @@ export class LocalAgentHost {
|
|
|
2058
2203
|
signal: abort.signal,
|
|
2059
2204
|
submissionObserved: () => live.forwarder.hasObservedSubmissionAfter(submissionCheckpoint, text),
|
|
2060
2205
|
});
|
|
2061
|
-
|
|
2206
|
+
if (!ok)
|
|
2207
|
+
return { outcome: "failed" };
|
|
2208
|
+
const responseId = pendingInput.responseId ?? live.currentResponseId();
|
|
2209
|
+
if (!responseId) {
|
|
2210
|
+
live.error = "Claude accepted the message but did not publish its native Turn identity";
|
|
2211
|
+
return { outcome: "failed" };
|
|
2212
|
+
}
|
|
2213
|
+
return { outcome: steered ? "steered" : "injected", responseId };
|
|
2062
2214
|
}
|
|
2063
2215
|
catch (error) {
|
|
2064
2216
|
live.error = error instanceof Error ? error.message : String(error);
|
|
2065
|
-
return "failed";
|
|
2217
|
+
return { outcome: "failed" };
|
|
2066
2218
|
}
|
|
2067
2219
|
finally {
|
|
2068
2220
|
if (live.injectAbort === abort)
|
|
@@ -2329,6 +2481,21 @@ function sameSessionSnapshots(workspace, execution, opts) {
|
|
|
2329
2481
|
execution: opts.execution,
|
|
2330
2482
|
}));
|
|
2331
2483
|
}
|
|
2484
|
+
function sameImmutableSessionSnapshots(workspace, execution, opts) {
|
|
2485
|
+
const current = { ...execution, model: null, reasoningEffort: null };
|
|
2486
|
+
const requested = { ...opts.execution, model: null, reasoningEffort: null };
|
|
2487
|
+
return JSON.stringify(stableValue({ workspace, execution: current })) ===
|
|
2488
|
+
JSON.stringify(stableValue({ workspace: opts.workspace, execution: requested }));
|
|
2489
|
+
}
|
|
2490
|
+
function updateLiveCodexTurnSettings(live, opts) {
|
|
2491
|
+
if (!sameImmutableSessionSnapshots(live.workspace, live.execution, opts)) {
|
|
2492
|
+
return false;
|
|
2493
|
+
}
|
|
2494
|
+
live.execution = structuredClone(opts.execution);
|
|
2495
|
+
live.model = opts.execution.model ?? "";
|
|
2496
|
+
live.reasoningEffort = opts.execution.reasoningEffort ?? undefined;
|
|
2497
|
+
return true;
|
|
2498
|
+
}
|
|
2332
2499
|
/** Freeze the Session's permission choice into the explicit managed settings.
|
|
2333
2500
|
* Host/project settings may still contribute allow/deny rules, but changing
|
|
2334
2501
|
* their defaultMode cannot silently mutate an existing Session on resume. */
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type { CodexCapabilities, CapabilityResult, CodexRuntimeStatus, } from ".
|
|
|
9
9
|
export type { ClaudeForkIntent, CodexSessionStore, CodexSessionRecord, } from "./codex-session-store.js";
|
|
10
10
|
export { RunnerManager, TerminalOpenError } from "./runner/manager.js";
|
|
11
11
|
export type { RunnerManagerOptions, RunnerSessionContext, RunnerSessionContextProvider, OpenTerminalOptions, ParentTerminal, } from "./runner/manager.js";
|
|
12
|
-
export type { InjectOutcome, TerminalOpenErrorCode, TerminalRole } from "./runner/protocol.js";
|
|
12
|
+
export type { InjectOutcome, InjectResult, TerminalOpenErrorCode, TerminalRole } from "./runner/protocol.js";
|
|
13
13
|
export type { ResolveInteractionResult, RuntimeInteractionEvent, RuntimeInteractionListener, } from "./interactions.js";
|
|
14
14
|
export { probeRuntimeStatus } from "./runtime-status.js";
|
|
15
15
|
export { listRuntimeModels } from "./models-catalog.js";
|
package/dist/runner/child.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { type AgentCapabilities, type LiveSessionFailure, type ResolvedExecution
|
|
|
11
11
|
import type { SessionEvent } from "@rynx-ai/core";
|
|
12
12
|
import type { TerminalInjector } from "../claude/native-integration.js";
|
|
13
13
|
import type { ResolveInteractionResult } from "../interactions.js";
|
|
14
|
-
import { type
|
|
14
|
+
import { type InjectResult } from "./protocol.js";
|
|
15
15
|
import type { ChildTransport } from "./transport.js";
|
|
16
16
|
/** Re-target the mirror to a freshly minted rynx session (claude `/clear`·`/fork`)
|
|
17
17
|
* + record the terminal transfer with the daemon. The child owns the transport,
|
|
@@ -50,7 +50,11 @@ interface LiveCodexProvider {
|
|
|
50
50
|
liveSessionFailure?(localThreadId: string): LiveSessionFailure | undefined;
|
|
51
51
|
failLiveStartup?(localThreadId: string, error: Error): boolean;
|
|
52
52
|
failLiveSession?(localThreadId: string, error: Error): boolean;
|
|
53
|
-
|
|
53
|
+
/** Codex/Traex auxiliary Terminal lifecycle: settle the current Turn, stop
|
|
54
|
+
* observer/forwarder, and evict the owned app-server while preserving the
|
|
55
|
+
* durable native-session binding for a later cold resume. */
|
|
56
|
+
teardownLiveCodexSession?(localThreadId: string, error?: Error): boolean;
|
|
57
|
+
injectMessage?(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectResult>;
|
|
54
58
|
interruptLive?(localThreadId: string): Promise<boolean>;
|
|
55
59
|
stopLiveCodexSession?(localThreadId: string, opts?: {
|
|
56
60
|
deferClaudeInteractionCleanup?: boolean;
|