@rynx-ai/runtime 0.1.11-beta.22 → 0.1.11-beta.23
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-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 +153 -71
- 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 +5 -9
- package/dist/runner/manager.js +16 -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
|
@@ -22,6 +22,11 @@ export interface ClaudeForwarderSink {
|
|
|
22
22
|
/** The current turn finished (a new user prompt, or the inactivity backstop).
|
|
23
23
|
* `usage` carries the latest statusLine context/cost snapshot, when captured. */
|
|
24
24
|
onTurnEnd(usage?: Record<string, unknown>): void;
|
|
25
|
+
/** The current turn ended because the user explicitly interrupted it. */
|
|
26
|
+
onTurnInterrupted?(usage?: Record<string, unknown>): void;
|
|
27
|
+
/** Escape was sent for the open Turn. Publish cancelled UI state immediately;
|
|
28
|
+
* the final close remains delayed so a late transcript record can join it. */
|
|
29
|
+
onTurnInterruptRequested?(): void;
|
|
25
30
|
/** The runtime status changed according to Claude's session metadata. */
|
|
26
31
|
onStatus?(status: ClaudeRunnerStatus, blockedOn?: string): void;
|
|
27
32
|
/** The runtime went idle on the legacy hook fallback — surface idle WITHOUT finalizing
|
|
@@ -102,6 +107,8 @@ export declare class ClaudeLiveSession {
|
|
|
102
107
|
private readonly seenClaudeSessionIds;
|
|
103
108
|
private turnOpen;
|
|
104
109
|
private currentTurnId?;
|
|
110
|
+
/** The open turn received an explicit Escape/Stop and must close cancelled. */
|
|
111
|
+
private turnInterrupted;
|
|
105
112
|
/** The Turn opened from a pre-transcript interaction. Its unique provisional
|
|
106
113
|
* id keeps responses distinct until the real prompt uuid can be adopted. */
|
|
107
114
|
private syntheticTurn;
|
|
@@ -27,6 +27,10 @@ const MAX_SETTLED_INTERACTIONS = 512;
|
|
|
27
27
|
const MAX_MESSAGE_CORRELATION_BACKLOG = 64;
|
|
28
28
|
const MAX_SUBMISSION_OBSERVATIONS = 64;
|
|
29
29
|
const INTERACTION_ACK_TIMEOUT_MS = 5_000;
|
|
30
|
+
// Claude writes this synthetic user record after Escape (including a tool-use
|
|
31
|
+
// interruption). It is lifecycle, not a new prompt. Kept aligned with
|
|
32
|
+
// Omnigent's `_CLAUDE_INTERRUPT_RECORD_RE`.
|
|
33
|
+
const CLAUDE_INTERRUPT_RECORD_RE = /^\[Request interrupted by user(?: for tool use)?\]$/;
|
|
30
34
|
const INTERACTION_LEASE_TIMEOUT_MS = 30_000;
|
|
31
35
|
function processIsAlive(pid) {
|
|
32
36
|
try {
|
|
@@ -165,6 +169,8 @@ export class ClaudeLiveSession {
|
|
|
165
169
|
seenClaudeSessionIds = new Set();
|
|
166
170
|
turnOpen = false;
|
|
167
171
|
currentTurnId;
|
|
172
|
+
/** The open turn received an explicit Escape/Stop and must close cancelled. */
|
|
173
|
+
turnInterrupted = false;
|
|
168
174
|
/** The Turn opened from a pre-transcript interaction. Its unique provisional
|
|
169
175
|
* id keeps responses distinct until the real prompt uuid can be adopted. */
|
|
170
176
|
syntheticTurn = false;
|
|
@@ -846,6 +852,12 @@ export class ClaudeLiveSession {
|
|
|
846
852
|
}
|
|
847
853
|
const content = userStringContent(rec);
|
|
848
854
|
if (content !== undefined) {
|
|
855
|
+
const firstLine = content.trim().split("\n", 1)[0] ?? "";
|
|
856
|
+
if (CLAUDE_INTERRUPT_RECORD_RE.test(firstLine)) {
|
|
857
|
+
this.noteInterrupted();
|
|
858
|
+
this.lastActivityAt = this.now();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
849
861
|
const candidates = submissionCandidates(content);
|
|
850
862
|
const queuedPromotion = rec.promptSource !== "typed" &&
|
|
851
863
|
candidates.some((candidate) => this.consumeQueuedPromotion(candidate));
|
|
@@ -890,6 +902,7 @@ export class ClaudeLiveSession {
|
|
|
890
902
|
this.closeTurn();
|
|
891
903
|
this.currentTurnId = turnId;
|
|
892
904
|
this.turnOpen = true;
|
|
905
|
+
this.turnInterrupted = false;
|
|
893
906
|
this.syntheticTurn = false;
|
|
894
907
|
this.providerIdleAt = null;
|
|
895
908
|
this.openToolIds.clear();
|
|
@@ -1100,6 +1113,7 @@ export class ClaudeLiveSession {
|
|
|
1100
1113
|
}
|
|
1101
1114
|
this.providerIdleAt = null;
|
|
1102
1115
|
this.turnOpen = true;
|
|
1116
|
+
this.turnInterrupted = false;
|
|
1103
1117
|
this.sink.onTurnStart(this.currentTurnId);
|
|
1104
1118
|
}
|
|
1105
1119
|
/** Mirror a local `!` command as its own mini-turn: close any open turn, then
|
|
@@ -1120,8 +1134,10 @@ export class ClaudeLiveSession {
|
|
|
1120
1134
|
* turn's close via the same short grace. The Escape has stopped claude, so this
|
|
1121
1135
|
* does not race a still-running response. */
|
|
1122
1136
|
noteInterrupted() {
|
|
1123
|
-
if (!this.turnOpen)
|
|
1137
|
+
if (!this.turnOpen || this.turnInterrupted)
|
|
1124
1138
|
return;
|
|
1139
|
+
this.turnInterrupted = true;
|
|
1140
|
+
this.sink.onTurnInterruptRequested?.();
|
|
1125
1141
|
this.cancelPendingInteractions("turn_interrupted");
|
|
1126
1142
|
this.stopSignalPending = false;
|
|
1127
1143
|
if (this.statusPoller?.active)
|
|
@@ -1160,7 +1176,13 @@ export class ClaudeLiveSession {
|
|
|
1160
1176
|
this.stopSignalPending = false;
|
|
1161
1177
|
this.stopPendingAt = null;
|
|
1162
1178
|
this.resetMessageCorrelation();
|
|
1163
|
-
this.
|
|
1179
|
+
const interrupted = this.turnInterrupted;
|
|
1180
|
+
this.turnInterrupted = false;
|
|
1181
|
+
const usage = this.statusUsage();
|
|
1182
|
+
if (interrupted && this.sink.onTurnInterrupted)
|
|
1183
|
+
this.sink.onTurnInterrupted(usage);
|
|
1184
|
+
else
|
|
1185
|
+
this.sink.onTurnEnd(usage);
|
|
1164
1186
|
}
|
|
1165
1187
|
closeTurnError(error) {
|
|
1166
1188
|
if (!this.turnOpen)
|
|
@@ -1171,6 +1193,7 @@ export class ClaudeLiveSession {
|
|
|
1171
1193
|
this.currentTurnId = undefined;
|
|
1172
1194
|
this.syntheticTurn = false;
|
|
1173
1195
|
this.openToolIds.clear();
|
|
1196
|
+
this.turnInterrupted = false;
|
|
1174
1197
|
this.stopSignalPending = false;
|
|
1175
1198
|
this.stopPendingAt = null;
|
|
1176
1199
|
this.resetMessageCorrelation();
|
|
@@ -39,6 +39,8 @@ export interface CodexForwarderSink {
|
|
|
39
39
|
onStatus?(note: string | undefined, statusKind?: "startup"): void;
|
|
40
40
|
/** The current turn finished; `usage` is the runtime's raw snapshot if any. */
|
|
41
41
|
onTurnEnd(usage?: Record<string, unknown>, reason?: "superseded"): void;
|
|
42
|
+
/** The provider confirmed that the active turn was explicitly interrupted. */
|
|
43
|
+
onTurnInterrupted?(usage?: Record<string, unknown>): void;
|
|
42
44
|
/** Resume proved that the newest turn is terminal even though its live edge
|
|
43
45
|
* was missed. This updates session state without replaying historical items. */
|
|
44
46
|
onRecoveredTurnStatus?(status: "idle" | "failed", turnId: string | undefined, error?: Error): void;
|
|
@@ -299,7 +299,9 @@ export class CodexSessionForwarder {
|
|
|
299
299
|
}
|
|
300
300
|
this.scheduleCompletion(mapped.fatalError
|
|
301
301
|
? { kind: "error", error: mapped.fatalError }
|
|
302
|
-
:
|
|
302
|
+
: mapped.turnInterrupted
|
|
303
|
+
? { kind: "interrupted", ...(mapped.usage ? { usage: mapped.usage } : {}) }
|
|
304
|
+
: { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
|
|
303
305
|
return;
|
|
304
306
|
}
|
|
305
307
|
// A late event from an older turn must not replace the app-server's active
|
|
@@ -359,7 +361,9 @@ export class CodexSessionForwarder {
|
|
|
359
361
|
return;
|
|
360
362
|
}
|
|
361
363
|
if (mapped.turnCompleted) {
|
|
362
|
-
this.scheduleCompletion(
|
|
364
|
+
this.scheduleCompletion(mapped.turnInterrupted
|
|
365
|
+
? { kind: "interrupted", ...(mapped.usage ? { usage: mapped.usage } : {}) }
|
|
366
|
+
: { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
|
|
363
367
|
}
|
|
364
368
|
else {
|
|
365
369
|
this.refreshCompletionGrace();
|
|
@@ -401,6 +405,12 @@ export class CodexSessionForwarder {
|
|
|
401
405
|
this.pendingCompletionTurnId = null;
|
|
402
406
|
if (completion.kind === "error")
|
|
403
407
|
this.sink.onTurnError(completion.error);
|
|
408
|
+
else if (completion.kind === "interrupted") {
|
|
409
|
+
if (this.sink.onTurnInterrupted)
|
|
410
|
+
this.sink.onTurnInterrupted(completion.usage);
|
|
411
|
+
else
|
|
412
|
+
this.sink.onTurnEnd(completion.usage);
|
|
413
|
+
}
|
|
404
414
|
else
|
|
405
415
|
this.sink.onTurnEnd(completion.usage);
|
|
406
416
|
}
|
|
@@ -241,7 +241,14 @@ export function mapCodexNotification(method, params) {
|
|
|
241
241
|
case "turn/failed": {
|
|
242
242
|
const turnPayload = typed.params?.turn;
|
|
243
243
|
const fatalError = terminalTurnError(turnPayload, typed.method);
|
|
244
|
-
|
|
244
|
+
const turnInterrupted = typed.method === "turn/completed" &&
|
|
245
|
+
["interrupted", "cancelled", "canceled"].includes(codexTurnStatus(turnPayload) ?? "");
|
|
246
|
+
return {
|
|
247
|
+
events,
|
|
248
|
+
...(fatalError ? { fatalError } : {}),
|
|
249
|
+
turnCompleted: true,
|
|
250
|
+
...(turnInterrupted ? { turnInterrupted: true } : {}),
|
|
251
|
+
};
|
|
245
252
|
}
|
|
246
253
|
case "turn/plan/updated": {
|
|
247
254
|
const planParams = typed.params;
|
package/dist/host.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type ResolvedExecutionSnapshot, type ResolvedExecutionBudget, type Runt
|
|
|
2
2
|
import { type AgentRuntimeId } from "@rynx-ai/core";
|
|
3
3
|
import { type AppConfig } from "@rynx-ai/core";
|
|
4
4
|
import { createCodexChildEnv } from "./codex-child-env.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { InjectResult } from "./runner/protocol.js";
|
|
6
6
|
import type { ResolveInteractionResult } from "./interactions.js";
|
|
7
7
|
import { CodexAppServerClient } from "./codex-app-server/client.js";
|
|
8
8
|
import type { ModelListResponse, ThreadGoal } from "./codex-app-server/protocol.js";
|
|
@@ -267,13 +267,13 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
267
267
|
* resumed from the persisted native id. Serialized per session so two
|
|
268
268
|
* injects can't double-open a turn.
|
|
269
269
|
*
|
|
270
|
-
* Returns an {@link
|
|
270
|
+
* Returns an {@link InjectResult}: `notLive` when this session has no live
|
|
271
271
|
* forwarder (caller may use the run path); `notReady`/`failed` are hard errors
|
|
272
272
|
* the caller reports WITHOUT re-running (re-running double-writes alongside the
|
|
273
273
|
* forwarder). Park-until-ready (~60s), aligning reference implementation's executor waiting for
|
|
274
274
|
* the bridge instead of a short race that falls back to a second output path.
|
|
275
275
|
*/
|
|
276
|
-
injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<
|
|
276
|
+
injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectResult>;
|
|
277
277
|
/**
|
|
278
278
|
* Interrupt the session's active turn — the web Stop button. codex: the
|
|
279
279
|
* app-server `turn/interrupt` on the active `{threadId, turnId}` (exactly what
|
|
@@ -287,6 +287,11 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
287
287
|
stopLiveCodexSession(localThreadId: string, opts?: {
|
|
288
288
|
deferClaudeInteractionCleanup?: boolean;
|
|
289
289
|
}): void;
|
|
290
|
+
/** Tear down one codex-lineage native runtime without deleting its durable
|
|
291
|
+
* session-store binding. Omnigent couples its auxiliary Terminal, observer,
|
|
292
|
+
* forwarder and per-session app-server as one disposable runtime envelope;
|
|
293
|
+
* the next message recreates that envelope and cold-resumes the native id. */
|
|
294
|
+
teardownLiveCodexSession(localThreadId: string, error?: Error): boolean;
|
|
290
295
|
/** Complete the second shutdown phase after the runner has killed all native
|
|
291
296
|
* terminals and hook subprocesses. Must run before the runner process exits. */
|
|
292
297
|
finalizeStoppedLiveSessions(): void;
|
|
@@ -308,7 +313,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
308
313
|
* serialized per session. Parks until the thread is ready AND the tmux injector
|
|
309
314
|
* is (re)attached, then pastes; `injectViaTerminal` RAISES if the prompt never
|
|
310
315
|
* appears (reference implementation RAISE), so a not-ready pane is a hard error — NOT a
|
|
311
|
-
* fall-through-to-run signal. Returns {@link
|
|
316
|
+
* fall-through-to-run signal. Returns {@link InjectResult}. */
|
|
312
317
|
private injectClaude;
|
|
313
318
|
/** Park until the claude session's tmux injector is (re)attached by the
|
|
314
319
|
* runner-child, or the deadline passes. Pane relaunch re-attaches it via
|
package/dist/host.js
CHANGED
|
@@ -681,6 +681,8 @@ export class LocalAgentHost {
|
|
|
681
681
|
injectLock: Promise.resolve(),
|
|
682
682
|
pendingInjectedInputs: [],
|
|
683
683
|
publishInjectedInput: () => undefined,
|
|
684
|
+
publishInterrupted: () => undefined,
|
|
685
|
+
interruptedResponseId: null,
|
|
684
686
|
subscribing: false,
|
|
685
687
|
rotationPending: false,
|
|
686
688
|
stopped: false,
|
|
@@ -698,11 +700,20 @@ export class LocalAgentHost {
|
|
|
698
700
|
}
|
|
699
701
|
emit(event);
|
|
700
702
|
};
|
|
703
|
+
live.publishInterrupted = (responseId) => {
|
|
704
|
+
if (live.interruptedResponseId === responseId)
|
|
705
|
+
return;
|
|
706
|
+
live.interruptedResponseId = responseId;
|
|
707
|
+
emitCurrent({
|
|
708
|
+
type: "session.interrupted",
|
|
709
|
+
sessionId: currentSessionId,
|
|
710
|
+
responseId,
|
|
711
|
+
});
|
|
712
|
+
};
|
|
701
713
|
let normalizer = null;
|
|
702
714
|
let currentResponseId = null;
|
|
703
715
|
const startNormalizer = (turnId) => {
|
|
704
|
-
const responseId =
|
|
705
|
-
(turnId ? `resp_codex_${turnId}` : "resp_codex_native");
|
|
716
|
+
const responseId = turnId ? `resp_codex_${turnId}` : "resp_codex_native";
|
|
706
717
|
if (normalizer && currentResponseId === responseId)
|
|
707
718
|
return normalizer;
|
|
708
719
|
currentResponseId = responseId;
|
|
@@ -842,6 +853,22 @@ export class LocalAgentHost {
|
|
|
842
853
|
normalizer = null;
|
|
843
854
|
currentResponseId = null;
|
|
844
855
|
};
|
|
856
|
+
const interruptCurrentTurn = () => {
|
|
857
|
+
closeCanonicalInteractions();
|
|
858
|
+
if (!normalizer)
|
|
859
|
+
return;
|
|
860
|
+
const interruptedResponseId = currentResponseId;
|
|
861
|
+
for (const se of normalizer.interrupt()) {
|
|
862
|
+
if (se.type === "session.interrupted" &&
|
|
863
|
+
live.interruptedResponseId === interruptedResponseId)
|
|
864
|
+
continue;
|
|
865
|
+
emitCurrent(se);
|
|
866
|
+
}
|
|
867
|
+
live.interruptedResponseId = null;
|
|
868
|
+
clearPendingInputsForResponse(interruptedResponseId);
|
|
869
|
+
normalizer = null;
|
|
870
|
+
currentResponseId = null;
|
|
871
|
+
};
|
|
845
872
|
const sink = {
|
|
846
873
|
onTurnStart: (turnId) => startNormalizer(turnId),
|
|
847
874
|
onTurnObserved: (turnId) => {
|
|
@@ -856,7 +883,7 @@ export class LocalAgentHost {
|
|
|
856
883
|
: content;
|
|
857
884
|
const signature = JSON.stringify(normalizedContent);
|
|
858
885
|
const pending = live.pendingInjectedInputs.find((entry) => entry.signature === signature);
|
|
859
|
-
if (pending?.state === "optimistic"
|
|
886
|
+
if (pending?.state === "optimistic") {
|
|
860
887
|
live.pendingInjectedInputs.splice(live.pendingInjectedInputs.indexOf(pending), 1);
|
|
861
888
|
return;
|
|
862
889
|
}
|
|
@@ -887,6 +914,7 @@ export class LocalAgentHost {
|
|
|
887
914
|
// response without publishing a false idle edge in a running→running
|
|
888
915
|
// transition.
|
|
889
916
|
onTurnEnd: completeCurrentTurn,
|
|
917
|
+
onTurnInterrupted: interruptCurrentTurn,
|
|
890
918
|
onRecoveredTurnStatus: (status, turnId, error) => {
|
|
891
919
|
const responseId = turnId ? `resp_codex_${turnId}` : undefined;
|
|
892
920
|
if (normalizer && (!responseId || currentResponseId === responseId)) {
|
|
@@ -1132,12 +1160,7 @@ export class LocalAgentHost {
|
|
|
1132
1160
|
return;
|
|
1133
1161
|
live.startupError = error;
|
|
1134
1162
|
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
|
-
});
|
|
1163
|
+
this.teardownLiveCodexSession(localThreadId, error);
|
|
1141
1164
|
}
|
|
1142
1165
|
shouldIgnoreManagedForkThreadStarted(live, threadId, forkedFromId) {
|
|
1143
1166
|
const pending = live.managedFork;
|
|
@@ -1292,7 +1315,7 @@ export class LocalAgentHost {
|
|
|
1292
1315
|
* resumed from the persisted native id. Serialized per session so two
|
|
1293
1316
|
* injects can't double-open a turn.
|
|
1294
1317
|
*
|
|
1295
|
-
* Returns an {@link
|
|
1318
|
+
* Returns an {@link InjectResult}: `notLive` when this session has no live
|
|
1296
1319
|
* forwarder (caller may use the run path); `notReady`/`failed` are hard errors
|
|
1297
1320
|
* the caller reports WITHOUT re-running (re-running double-writes alongside the
|
|
1298
1321
|
* forwarder). Park-until-ready (~60s), aligning reference implementation's executor waiting for
|
|
@@ -1309,9 +1332,8 @@ export class LocalAgentHost {
|
|
|
1309
1332
|
const pendingInput = {
|
|
1310
1333
|
content,
|
|
1311
1334
|
signature: JSON.stringify(content),
|
|
1312
|
-
state:
|
|
1335
|
+
state: "awaiting",
|
|
1313
1336
|
observed: false,
|
|
1314
|
-
...(runtimeInput.responseId ? { responseId: runtimeInput.responseId } : {}),
|
|
1315
1337
|
};
|
|
1316
1338
|
claude.pendingInjectedInputs.push(pendingInput);
|
|
1317
1339
|
const forgetPendingInput = () => {
|
|
@@ -1325,8 +1347,17 @@ export class LocalAgentHost {
|
|
|
1325
1347
|
const expiry = setTimeout(() => claude.pendingImageInputs.delete(token), 5 * 60_000);
|
|
1326
1348
|
expiry.unref?.();
|
|
1327
1349
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1350
|
+
let result;
|
|
1351
|
+
try {
|
|
1352
|
+
result = await this.injectClaude(claude, localThreadId, text, pendingInput);
|
|
1353
|
+
}
|
|
1354
|
+
catch (error) {
|
|
1355
|
+
forgetPendingInput();
|
|
1356
|
+
if (token)
|
|
1357
|
+
claude.pendingImageInputs.delete(token);
|
|
1358
|
+
throw error;
|
|
1359
|
+
}
|
|
1360
|
+
if (result.outcome !== "injected" && result.outcome !== "steered") {
|
|
1330
1361
|
forgetPendingInput();
|
|
1331
1362
|
if (token)
|
|
1332
1363
|
claude.pendingImageInputs.delete(token);
|
|
@@ -1334,28 +1365,27 @@ export class LocalAgentHost {
|
|
|
1334
1365
|
else if (pendingInput.observed) {
|
|
1335
1366
|
forgetPendingInput();
|
|
1336
1367
|
}
|
|
1337
|
-
return
|
|
1368
|
+
return result;
|
|
1338
1369
|
}
|
|
1339
1370
|
const live = this.liveSessions.get(localThreadId);
|
|
1340
1371
|
if (!live)
|
|
1341
|
-
return "notLive";
|
|
1372
|
+
return { outcome: "notLive" };
|
|
1342
1373
|
const run = live.injectLock.then(async () => {
|
|
1343
1374
|
if (live.rotationPending || live.stopped)
|
|
1344
|
-
return "failed";
|
|
1375
|
+
return { outcome: "failed" };
|
|
1345
1376
|
// Park until the thread binds (~60s, reference implementation codex_native_executor:177-186),
|
|
1346
1377
|
// not a 20s race that returns false and lets the caller re-run on a 2nd path.
|
|
1347
1378
|
const bound = await this.waitLiveReady(localThreadId, CODEX_BRIDGE_READY_TIMEOUT_MS);
|
|
1348
1379
|
const threadId = live.threadId ?? live.forwarder.threadId();
|
|
1349
1380
|
if (!bound || !threadId)
|
|
1350
|
-
return "notReady";
|
|
1381
|
+
return { outcome: "notReady" };
|
|
1351
1382
|
const nativeInput = buildRuntimeUserInput(runtimeInput);
|
|
1352
1383
|
const content = runtimeUserContent(runtimeInput);
|
|
1353
1384
|
const pendingInput = {
|
|
1354
1385
|
content,
|
|
1355
1386
|
signature: JSON.stringify(content),
|
|
1356
|
-
state:
|
|
1387
|
+
state: "awaiting",
|
|
1357
1388
|
observed: false,
|
|
1358
|
-
...(runtimeInput.responseId ? { responseId: runtimeInput.responseId } : {}),
|
|
1359
1389
|
};
|
|
1360
1390
|
live.pendingInjectedInputs.push(pendingInput);
|
|
1361
1391
|
let injectionMethod = "turn/start";
|
|
@@ -1378,11 +1408,7 @@ export class LocalAgentHost {
|
|
|
1378
1408
|
expectedTurnId: turnId,
|
|
1379
1409
|
input: nativeInput,
|
|
1380
1410
|
});
|
|
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) {
|
|
1411
|
+
if (pendingInput.observed) {
|
|
1386
1412
|
forgetPendingInput();
|
|
1387
1413
|
}
|
|
1388
1414
|
else {
|
|
@@ -1390,7 +1416,7 @@ export class LocalAgentHost {
|
|
|
1390
1416
|
pendingInput.state = "optimistic";
|
|
1391
1417
|
}
|
|
1392
1418
|
live.forwarder.noteTurnAccepted(steered.turnId);
|
|
1393
|
-
return "steered";
|
|
1419
|
+
return { outcome: "steered", responseId: `resp_codex_${steered.turnId}` };
|
|
1394
1420
|
}
|
|
1395
1421
|
}
|
|
1396
1422
|
// Carry the agent-spec model on the turn so a web-injected turn runs the
|
|
@@ -1403,11 +1429,7 @@ export class LocalAgentHost {
|
|
|
1403
1429
|
...(live.model ? { model: live.model } : {}),
|
|
1404
1430
|
...(live.reasoningEffort ? { effort: live.reasoningEffort } : {}),
|
|
1405
1431
|
});
|
|
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) {
|
|
1432
|
+
if (pendingInput.observed) {
|
|
1411
1433
|
forgetPendingInput();
|
|
1412
1434
|
}
|
|
1413
1435
|
else {
|
|
@@ -1418,7 +1440,7 @@ export class LocalAgentHost {
|
|
|
1418
1440
|
// Do not wait for the independent observer connection's `turn/started`:
|
|
1419
1441
|
// a second message accepted in that window must steer, not double-start.
|
|
1420
1442
|
live.forwarder.noteTurnAccepted(started.turnId);
|
|
1421
|
-
return "injected";
|
|
1443
|
+
return { outcome: "injected", responseId: `resp_codex_${started.turnId}` };
|
|
1422
1444
|
}
|
|
1423
1445
|
catch (error) {
|
|
1424
1446
|
forgetPendingInput();
|
|
@@ -1501,6 +1523,7 @@ export class LocalAgentHost {
|
|
|
1501
1523
|
if (turnId) {
|
|
1502
1524
|
try {
|
|
1503
1525
|
await interruptClient.turnInterrupt({ threadId, turnId });
|
|
1526
|
+
live.publishInterrupted(`resp_codex_${turnId}`);
|
|
1504
1527
|
handled = true;
|
|
1505
1528
|
}
|
|
1506
1529
|
catch {
|
|
@@ -1555,6 +1578,27 @@ export class LocalAgentHost {
|
|
|
1555
1578
|
// Remove the session-scoped skills dir — the machine keeps zero task residue.
|
|
1556
1579
|
void live.skillsCleanup?.();
|
|
1557
1580
|
}
|
|
1581
|
+
/** Tear down one codex-lineage native runtime without deleting its durable
|
|
1582
|
+
* session-store binding. Omnigent couples its auxiliary Terminal, observer,
|
|
1583
|
+
* forwarder and per-session app-server as one disposable runtime envelope;
|
|
1584
|
+
* the next message recreates that envelope and cold-resumes the native id. */
|
|
1585
|
+
teardownLiveCodexSession(localThreadId, error) {
|
|
1586
|
+
const live = this.liveSessions.get(localThreadId);
|
|
1587
|
+
if (!live)
|
|
1588
|
+
return false;
|
|
1589
|
+
const turnFailed = error ? live.forwarder.failOpenTurn(error) : false;
|
|
1590
|
+
const backendKey = codexBackendKey(live.runtime, live.execution.budget ?? undefined);
|
|
1591
|
+
const backend = this.backends.get(backendKey);
|
|
1592
|
+
const appServerOwner = live.appServerOwner;
|
|
1593
|
+
this.stopLiveCodexSession(localThreadId);
|
|
1594
|
+
if (backend?.appServerClient === appServerOwner) {
|
|
1595
|
+
this.backends.delete(backendKey);
|
|
1596
|
+
}
|
|
1597
|
+
void appServerOwner.stop().catch((stopError) => {
|
|
1598
|
+
console.warn(`[codex-live] session=${localThreadId} runtime=${live.runtime} app-server teardown failed: ${stopError instanceof Error ? stopError.message : String(stopError)}`);
|
|
1599
|
+
});
|
|
1600
|
+
return turnFailed;
|
|
1601
|
+
}
|
|
1558
1602
|
/** Complete the second shutdown phase after the runner has killed all native
|
|
1559
1603
|
* terminals and hook subprocesses. Must run before the runner process exits. */
|
|
1560
1604
|
finalizeStoppedLiveSessions() {
|
|
@@ -1776,8 +1820,7 @@ export class LocalAgentHost {
|
|
|
1776
1820
|
};
|
|
1777
1821
|
const startNormalizer = (turnId) => {
|
|
1778
1822
|
// turnId unknown → fixed literal (never random), aligning reference implementation `_response_id`.
|
|
1779
|
-
const responseId =
|
|
1780
|
-
(turnId ? `resp_claude_${turnId}` : "resp_claude_native");
|
|
1823
|
+
const responseId = turnId ? `resp_claude_${turnId}` : "resp_claude_native";
|
|
1781
1824
|
if (normalizer && currentResponseId === responseId)
|
|
1782
1825
|
return normalizer;
|
|
1783
1826
|
currentResponseId = responseId;
|
|
@@ -1822,6 +1865,8 @@ export class LocalAgentHost {
|
|
|
1822
1865
|
injectLock: Promise.resolve(),
|
|
1823
1866
|
pendingImageInputs: new Map(),
|
|
1824
1867
|
pendingInjectedInputs: [],
|
|
1868
|
+
currentResponseId: () => currentResponseId,
|
|
1869
|
+
publishInterrupted: () => undefined,
|
|
1825
1870
|
ready,
|
|
1826
1871
|
markReady,
|
|
1827
1872
|
failed,
|
|
@@ -1833,6 +1878,55 @@ export class LocalAgentHost {
|
|
|
1833
1878
|
...(forkIntent ? { forkIntent } : {}),
|
|
1834
1879
|
...(skillPlugin?.cleanup ? { skillCleanup: skillPlugin.cleanup } : {}),
|
|
1835
1880
|
};
|
|
1881
|
+
live.publishInterrupted = (responseId) => {
|
|
1882
|
+
if (live.interruptedResponseId === responseId)
|
|
1883
|
+
return;
|
|
1884
|
+
live.interruptedResponseId = responseId;
|
|
1885
|
+
emitCurrent({
|
|
1886
|
+
type: "session.interrupted",
|
|
1887
|
+
sessionId: currentSessionId,
|
|
1888
|
+
responseId,
|
|
1889
|
+
});
|
|
1890
|
+
};
|
|
1891
|
+
const settleClaudeTurn = (interrupted, usage) => {
|
|
1892
|
+
if (!normalizer)
|
|
1893
|
+
return;
|
|
1894
|
+
const rid = currentResponseId;
|
|
1895
|
+
if (interrupted) {
|
|
1896
|
+
for (const se of normalizer.interrupt()) {
|
|
1897
|
+
if (se.type === "session.interrupted" &&
|
|
1898
|
+
live.interruptedResponseId === rid)
|
|
1899
|
+
continue;
|
|
1900
|
+
emitCurrent(se);
|
|
1901
|
+
}
|
|
1902
|
+
live.interruptedResponseId = undefined;
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
// statusLine usage (context/cost) rides the turn's response.completed.
|
|
1906
|
+
if (usage) {
|
|
1907
|
+
for (const se of normalizer.next({ type: "turn_completed", usage }))
|
|
1908
|
+
emitCurrent(se);
|
|
1909
|
+
}
|
|
1910
|
+
for (const se of normalizer.next({ type: "done" }))
|
|
1911
|
+
emitCurrent(se);
|
|
1912
|
+
}
|
|
1913
|
+
live.pendingInjectedInputs = live.pendingInjectedInputs.filter((entry) => entry.responseId !== rid);
|
|
1914
|
+
normalizer = null;
|
|
1915
|
+
currentResponseId = undefined;
|
|
1916
|
+
// One-time context-window banner past CONTEXT_WARN_RATIO (a transient note,
|
|
1917
|
+
// cleared by the next turn's first item). statusLine pre-computes the %.
|
|
1918
|
+
const pct = usage && typeof usage.used_percentage === "number" ? usage.used_percentage : undefined;
|
|
1919
|
+
if (!interrupted && rid && pct !== undefined && !live.contextWarned && pct >= CONTEXT_WARN_RATIO * 100) {
|
|
1920
|
+
live.contextWarned = true;
|
|
1921
|
+
emitCurrent({
|
|
1922
|
+
type: "session.status",
|
|
1923
|
+
sessionId: currentSessionId,
|
|
1924
|
+
responseId: rid,
|
|
1925
|
+
status: "idle",
|
|
1926
|
+
note: `context ${Math.round(pct)}% full — consider /compact`,
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1836
1930
|
const sink = {
|
|
1837
1931
|
onTurnStart: (turnId) => startNormalizer(turnId),
|
|
1838
1932
|
onUserMessage: (text) => {
|
|
@@ -1844,12 +1938,13 @@ export class LocalAgentHost {
|
|
|
1844
1938
|
const normalizedContent = content ?? [{ type: "input_text", text }];
|
|
1845
1939
|
const signature = JSON.stringify(normalizedContent);
|
|
1846
1940
|
const pending = live.pendingInjectedInputs.find((entry) => entry.signature === signature);
|
|
1847
|
-
if (pending
|
|
1941
|
+
if (pending) {
|
|
1942
|
+
pending.responseId = currentResponseId;
|
|
1848
1943
|
live.pendingInjectedInputs.splice(live.pendingInjectedInputs.indexOf(pending), 1);
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
if (pending)
|
|
1944
|
+
if (pending.state === "optimistic")
|
|
1945
|
+
return;
|
|
1852
1946
|
pending.observed = true;
|
|
1947
|
+
}
|
|
1853
1948
|
for (const se of n.userInput(normalizedContent))
|
|
1854
1949
|
emitCurrent(se);
|
|
1855
1950
|
},
|
|
@@ -1876,33 +1971,12 @@ export class LocalAgentHost {
|
|
|
1876
1971
|
...(blockedOn ? { note: blockedOn } : {}),
|
|
1877
1972
|
});
|
|
1878
1973
|
},
|
|
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
|
-
}
|
|
1974
|
+
onTurnEnd: (usage) => settleClaudeTurn(false, usage),
|
|
1975
|
+
onTurnInterrupted: (usage) => settleClaudeTurn(true, usage),
|
|
1976
|
+
onTurnInterruptRequested: () => {
|
|
1977
|
+
const responseId = currentResponseId;
|
|
1978
|
+
if (responseId)
|
|
1979
|
+
live.publishInterrupted(responseId);
|
|
1906
1980
|
},
|
|
1907
1981
|
onIdle: () => {
|
|
1908
1982
|
// Surface idle on the current turn WITHOUT finalizing it (see the sink's
|
|
@@ -2037,17 +2111,18 @@ export class LocalAgentHost {
|
|
|
2037
2111
|
* serialized per session. Parks until the thread is ready AND the tmux injector
|
|
2038
2112
|
* is (re)attached, then pastes; `injectViaTerminal` RAISES if the prompt never
|
|
2039
2113
|
* 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) {
|
|
2114
|
+
* fall-through-to-run signal. Returns {@link InjectResult}. */
|
|
2115
|
+
injectClaude(live, localThreadId, text, pendingInput) {
|
|
2042
2116
|
const run = live.injectLock.then(async () => {
|
|
2043
2117
|
const ready = await this.waitLiveReady(localThreadId, 60_000);
|
|
2044
2118
|
if (!ready)
|
|
2045
|
-
return "notReady";
|
|
2119
|
+
return { outcome: "notReady" };
|
|
2046
2120
|
// Pane may have just relaunched — park until its injector re-attaches
|
|
2047
2121
|
// (attachTerminalInjector resets it) instead of returning false → fallback.
|
|
2048
2122
|
const injector = await this.waitInjector(live, 60_000);
|
|
2049
2123
|
if (!injector)
|
|
2050
|
-
return "notReady";
|
|
2124
|
+
return { outcome: "notReady" };
|
|
2125
|
+
const steered = live.forwarder.isTurnOpen();
|
|
2051
2126
|
// Abortable: the web Stop button cancels an in-flight paste/submit (before
|
|
2052
2127
|
// the message reaches claude) via interruptLive → injectAbort.abort().
|
|
2053
2128
|
const abort = new AbortController();
|
|
@@ -2058,11 +2133,18 @@ export class LocalAgentHost {
|
|
|
2058
2133
|
signal: abort.signal,
|
|
2059
2134
|
submissionObserved: () => live.forwarder.hasObservedSubmissionAfter(submissionCheckpoint, text),
|
|
2060
2135
|
});
|
|
2061
|
-
|
|
2136
|
+
if (!ok)
|
|
2137
|
+
return { outcome: "failed" };
|
|
2138
|
+
const responseId = pendingInput.responseId ?? live.currentResponseId();
|
|
2139
|
+
if (!responseId) {
|
|
2140
|
+
live.error = "Claude accepted the message but did not publish its native Turn identity";
|
|
2141
|
+
return { outcome: "failed" };
|
|
2142
|
+
}
|
|
2143
|
+
return { outcome: steered ? "steered" : "injected", responseId };
|
|
2062
2144
|
}
|
|
2063
2145
|
catch (error) {
|
|
2064
2146
|
live.error = error instanceof Error ? error.message : String(error);
|
|
2065
|
-
return "failed";
|
|
2147
|
+
return { outcome: "failed" };
|
|
2066
2148
|
}
|
|
2067
2149
|
finally {
|
|
2068
2150
|
if (live.injectAbort === abort)
|
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";
|