@rynx-ai/runtime 0.1.11-beta.21 → 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/client.d.ts +4 -3
- package/dist/codex-app-server/client.js +35 -12
- package/dist/codex-app-server/forwarder.d.ts +2 -7
- package/dist/codex-app-server/forwarder.js +12 -50
- package/dist/codex-app-server/mapping.d.ts +1 -0
- package/dist/codex-app-server/mapping.js +8 -1
- package/dist/codex-app-server/protocol.d.ts +1 -2
- package/dist/host.d.ts +36 -25
- package/dist/host.js +384 -441
- package/dist/index.d.ts +1 -1
- package/dist/runner/child.d.ts +18 -7
- package/dist/runner/child.js +169 -45
- package/dist/runner/manager.d.ts +14 -15
- package/dist/runner/manager.js +90 -98
- package/dist/runner/protocol.d.ts +16 -15
- package/dist/runner/startup-policy.d.ts +3 -0
- package/dist/runner/startup-policy.js +5 -0
- 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();
|
|
@@ -29,9 +29,11 @@ export declare class CodexAppServerClient {
|
|
|
29
29
|
private interactionListener;
|
|
30
30
|
private connectionListener;
|
|
31
31
|
private connectionState;
|
|
32
|
+
private connectionEstablished;
|
|
32
33
|
private readonly pendingInteractions;
|
|
33
34
|
private readonly settledInteractions;
|
|
34
35
|
private initializeResponse;
|
|
36
|
+
private initializePromise;
|
|
35
37
|
constructor({ spawner, channel, logger, clientInfo, approvalDecisionPolicy, }: CodexAppServerClientOptions);
|
|
36
38
|
/**
|
|
37
39
|
* The multi-client endpoint a `codex --remote` TUI can attach to, when this
|
|
@@ -94,9 +96,8 @@ export declare class CodexAppServerClient {
|
|
|
94
96
|
* resolver still wins correctly.
|
|
95
97
|
*/
|
|
96
98
|
setInteractionListener(listener: RuntimeInteractionListener | null): void;
|
|
97
|
-
/** Observe the
|
|
98
|
-
*
|
|
99
|
-
* native request still has to count as unavailable during host failover. */
|
|
99
|
+
/** Observe the initialized connection lifecycle. Registration never reports
|
|
100
|
+
* disconnected for a client that has not connected yet. */
|
|
100
101
|
setConnectionListener(listener: ((state: "connected" | "disconnected") => void) | null): void;
|
|
101
102
|
private setConnectionState;
|
|
102
103
|
resolveInteraction(interactionId: string, resolution: SessionInteractionResolution): ResolveInteractionResult;
|
|
@@ -953,9 +953,11 @@ export class CodexAppServerClient {
|
|
|
953
953
|
interactionListener = null;
|
|
954
954
|
connectionListener = null;
|
|
955
955
|
connectionState = "disconnected";
|
|
956
|
+
connectionEstablished = false;
|
|
956
957
|
pendingInteractions = new Map();
|
|
957
958
|
settledInteractions = new Set();
|
|
958
959
|
initializeResponse = null;
|
|
960
|
+
initializePromise = null;
|
|
959
961
|
constructor({ spawner, channel, logger = defaultLogger, clientInfo = DEFAULT_CLIENT_INFO, approvalDecisionPolicy = "auto-approve-session", }) {
|
|
960
962
|
this.logger = logger;
|
|
961
963
|
this.clientInfo = clientInfo;
|
|
@@ -988,14 +990,31 @@ export class CodexAppServerClient {
|
|
|
988
990
|
if (this.initializeResponse) {
|
|
989
991
|
return this.initializeResponse;
|
|
990
992
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
993
|
+
if (this.initializePromise)
|
|
994
|
+
return this.initializePromise;
|
|
995
|
+
const initializing = (async () => {
|
|
996
|
+
await this.transport.ensureStarted();
|
|
997
|
+
const response = await this.transport.sendRequest("initialize", {
|
|
998
|
+
clientInfo: this.clientInfo,
|
|
999
|
+
capabilities: { experimentalApi: true },
|
|
1000
|
+
});
|
|
1001
|
+
// Codex app-server uses the full initialize handshake: it does not accept
|
|
1002
|
+
// capability requests after merely replying to `initialize`. The client
|
|
1003
|
+
// must acknowledge that response with the `initialized` notification
|
|
1004
|
+
// before `thread/resume`, `turn/start`, and the other APIs are legal.
|
|
1005
|
+
await this.transport.sendNotification("initialized");
|
|
1006
|
+
this.initializeResponse = response;
|
|
1007
|
+
this.setConnectionState("connected");
|
|
1008
|
+
return response;
|
|
1009
|
+
})();
|
|
1010
|
+
this.initializePromise = initializing;
|
|
1011
|
+
try {
|
|
1012
|
+
return await initializing;
|
|
1013
|
+
}
|
|
1014
|
+
finally {
|
|
1015
|
+
if (this.initializePromise === initializing)
|
|
1016
|
+
this.initializePromise = null;
|
|
1017
|
+
}
|
|
999
1018
|
}
|
|
1000
1019
|
async getAuthStatus(params = {}) {
|
|
1001
1020
|
await this.ensureInitialized();
|
|
@@ -1146,14 +1165,18 @@ export class CodexAppServerClient {
|
|
|
1146
1165
|
setInteractionListener(listener) {
|
|
1147
1166
|
this.interactionListener = listener;
|
|
1148
1167
|
}
|
|
1149
|
-
/** Observe the
|
|
1150
|
-
*
|
|
1151
|
-
* native request still has to count as unavailable during host failover. */
|
|
1168
|
+
/** Observe the initialized connection lifecycle. Registration never reports
|
|
1169
|
+
* disconnected for a client that has not connected yet. */
|
|
1152
1170
|
setConnectionListener(listener) {
|
|
1153
1171
|
this.connectionListener = listener;
|
|
1154
|
-
listener
|
|
1172
|
+
if (listener && this.connectionEstablished)
|
|
1173
|
+
listener(this.connectionState);
|
|
1155
1174
|
}
|
|
1156
1175
|
setConnectionState(state) {
|
|
1176
|
+
if (state === "connected")
|
|
1177
|
+
this.connectionEstablished = true;
|
|
1178
|
+
if (state === "disconnected" && !this.connectionEstablished)
|
|
1179
|
+
return;
|
|
1157
1180
|
if (this.connectionState === state)
|
|
1158
1181
|
return;
|
|
1159
1182
|
this.connectionState = state;
|
|
@@ -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;
|
|
@@ -134,13 +136,6 @@ export declare class CodexSessionForwarder {
|
|
|
134
136
|
* not doubled.
|
|
135
137
|
*/
|
|
136
138
|
replayBackfill(turns: ResumedTurn[]): void;
|
|
137
|
-
/** Reconcile only the exact active turn after an observer resume. Historical
|
|
138
|
-
* items are intentionally not replayed on an existing-thread reconnect. */
|
|
139
|
-
reconcileActiveTurn(turn: ResumedTurn | undefined): boolean;
|
|
140
|
-
/** Reconcile an observer reconnect from the explicit resume snapshot. An
|
|
141
|
-
* identified active turn must match exactly; with no active turn, only the
|
|
142
|
-
* newest explicit terminal status is published and no history is replayed. */
|
|
143
|
-
reconcileResumeTurns(turns: ResumedTurn[]): boolean;
|
|
144
139
|
/** Fail an open response exactly once when its observer or terminal exits. */
|
|
145
140
|
failOpenTurn(error: Error): boolean;
|
|
146
141
|
private handle;
|
|
@@ -209,54 +209,6 @@ export class CodexSessionForwarder {
|
|
|
209
209
|
this.sink.onTurnEnd(mapped.usage);
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
-
/** Reconcile only the exact active turn after an observer resume. Historical
|
|
213
|
-
* items are intentionally not replayed on an existing-thread reconnect. */
|
|
214
|
-
reconcileActiveTurn(turn) {
|
|
215
|
-
if (!this.isTurnOpen() || !turn || codexResumeTerminalStatus(turn) === undefined)
|
|
216
|
-
return false;
|
|
217
|
-
const resumedTurnId = turn.id ?? turn.turnId;
|
|
218
|
-
if (!this.currentTurnIdValue || resumedTurnId !== this.currentTurnIdValue)
|
|
219
|
-
return false;
|
|
220
|
-
const mapped = mapCodexNotification("turn/completed", { turn });
|
|
221
|
-
this.ensureTurn();
|
|
222
|
-
this.settle(mapped.fatalError
|
|
223
|
-
? { kind: "error", error: mapped.fatalError }
|
|
224
|
-
: { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
/** Reconcile an observer reconnect from the explicit resume snapshot. An
|
|
228
|
-
* identified active turn must match exactly; with no active turn, only the
|
|
229
|
-
* newest explicit terminal status is published and no history is replayed. */
|
|
230
|
-
reconcileResumeTurns(turns) {
|
|
231
|
-
if (this.currentThreadIdValue === null)
|
|
232
|
-
return false;
|
|
233
|
-
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
234
|
-
const turn = turns[index];
|
|
235
|
-
if (!turn)
|
|
236
|
-
continue;
|
|
237
|
-
const turnId = turn.id ?? turn.turnId;
|
|
238
|
-
if (!turnId)
|
|
239
|
-
return false;
|
|
240
|
-
if (this.currentTurnIdValue !== null && this.currentTurnIdValue !== turnId) {
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
const status = codexResumeTerminalStatus(turn);
|
|
244
|
-
if (!status)
|
|
245
|
-
return false;
|
|
246
|
-
const mapped = mapCodexNotification("turn/completed", { turn });
|
|
247
|
-
if (this.currentTurnIdValue !== null || this.turnOpen) {
|
|
248
|
-
this.ensureTurn();
|
|
249
|
-
this.settle(mapped.fatalError
|
|
250
|
-
? { kind: "error", error: mapped.fatalError }
|
|
251
|
-
: { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
this.sink.onRecoveredTurnStatus?.(status, turnId, mapped.fatalError);
|
|
255
|
-
}
|
|
256
|
-
return true;
|
|
257
|
-
}
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
212
|
/** Fail an open response exactly once when its observer or terminal exits. */
|
|
261
213
|
failOpenTurn(error) {
|
|
262
214
|
if (!this.isTurnOpen())
|
|
@@ -347,7 +299,9 @@ export class CodexSessionForwarder {
|
|
|
347
299
|
}
|
|
348
300
|
this.scheduleCompletion(mapped.fatalError
|
|
349
301
|
? { kind: "error", error: mapped.fatalError }
|
|
350
|
-
:
|
|
302
|
+
: mapped.turnInterrupted
|
|
303
|
+
? { kind: "interrupted", ...(mapped.usage ? { usage: mapped.usage } : {}) }
|
|
304
|
+
: { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
|
|
351
305
|
return;
|
|
352
306
|
}
|
|
353
307
|
// A late event from an older turn must not replace the app-server's active
|
|
@@ -407,7 +361,9 @@ export class CodexSessionForwarder {
|
|
|
407
361
|
return;
|
|
408
362
|
}
|
|
409
363
|
if (mapped.turnCompleted) {
|
|
410
|
-
this.scheduleCompletion(
|
|
364
|
+
this.scheduleCompletion(mapped.turnInterrupted
|
|
365
|
+
? { kind: "interrupted", ...(mapped.usage ? { usage: mapped.usage } : {}) }
|
|
366
|
+
: { kind: "end", ...(mapped.usage ? { usage: mapped.usage } : {}) });
|
|
411
367
|
}
|
|
412
368
|
else {
|
|
413
369
|
this.refreshCompletionGrace();
|
|
@@ -449,6 +405,12 @@ export class CodexSessionForwarder {
|
|
|
449
405
|
this.pendingCompletionTurnId = null;
|
|
450
406
|
if (completion.kind === "error")
|
|
451
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
|
+
}
|
|
452
414
|
else
|
|
453
415
|
this.sink.onTurnEnd(completion.usage);
|
|
454
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;
|
|
@@ -105,8 +105,7 @@ export interface ThreadStartParams {
|
|
|
105
105
|
}
|
|
106
106
|
export interface ThreadResumeParams extends ThreadStartParams {
|
|
107
107
|
threadId: string;
|
|
108
|
-
/**
|
|
109
|
-
* `initialTurnsPage` so recovery can fetch one summarized terminal status. */
|
|
108
|
+
/** Suppress rollout history when the caller only needs to load/subscribe. */
|
|
110
109
|
excludeTurns?: boolean;
|
|
111
110
|
initialTurnsPage?: {
|
|
112
111
|
limit?: number | null;
|
package/dist/host.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type ResolvedExecutionSnapshot, type ResolvedExecutionBudget, type RuntimeUserInput, type SessionWorkspaceSnapshot, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
|
|
1
|
+
import { type ResolvedExecutionSnapshot, type ResolvedExecutionBudget, type RuntimeUserInput, type LiveSessionFailure, type SessionWorkspaceSnapshot, type SessionInteractionResolution, type SessionEvent } from "@rynx-ai/core";
|
|
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";
|
|
@@ -125,7 +125,9 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
125
125
|
private readonly clock;
|
|
126
126
|
private readonly backendIdleTtlMs;
|
|
127
127
|
private readonly forwarderClientFactory;
|
|
128
|
-
private readonly
|
|
128
|
+
private readonly preloadClientFactory;
|
|
129
|
+
/** Builds one short-lived message/interrupt client. */
|
|
130
|
+
private readonly injectionClientFactory;
|
|
129
131
|
private readonly backends;
|
|
130
132
|
private readonly sessionId;
|
|
131
133
|
private readonly runtimeHomeSessionId;
|
|
@@ -146,7 +148,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
146
148
|
/** Short-lived dedupe for managed fork notifications delivered after the
|
|
147
149
|
* `thread/fork` response. Values are expected source Provider thread ids. */
|
|
148
150
|
private readonly managedForkThreadStarts;
|
|
149
|
-
constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, now, backendIdleTtlMs, forwarderClientFactory,
|
|
151
|
+
constructor({ config, commandRunner, sessionStore, allowedRoots, appServerClient, now, backendIdleTtlMs, forwarderClientFactory, preloadClientFactory, injectionClientFactory, sessionId, runtimeHomeSessionId, }: {
|
|
150
152
|
config: AppConfig;
|
|
151
153
|
commandRunner?: CodexCommandRunner;
|
|
152
154
|
sessionStore?: CodexSessionStore;
|
|
@@ -158,8 +160,10 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
158
160
|
backendIdleTtlMs?: number;
|
|
159
161
|
/** Override the live-session forwarder connection factory (tests inject a fake). */
|
|
160
162
|
forwarderClientFactory?: (appServerUrl: string) => CodexAppServerClient;
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
+
/** Override the short-lived known-thread preload connection factory. */
|
|
164
|
+
preloadClientFactory?: (appServerUrl: string) => CodexAppServerClient;
|
|
165
|
+
/** Override the short-lived message/interrupt connection factory. */
|
|
166
|
+
injectionClientFactory?: (appServerUrl: string) => CodexAppServerClient;
|
|
163
167
|
/** The rynx session (localThreadId) this host serves — a per-session runner
|
|
164
168
|
* child sets it from `RYNX_RUNNER_SESSION` so the private CODEX_HOME is
|
|
165
169
|
* session-scoped. The shared `__cap__` child / tests fall back to a sentinel
|
|
@@ -219,33 +223,39 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
219
223
|
/** Bind a session's codex thread id once known (TUI broadcast or store): persist
|
|
220
224
|
* it, unblock injection, and kick off the resume-subscribe loop (once). */
|
|
221
225
|
private onLiveThreadStarted;
|
|
226
|
+
/** Start the known-thread observer after the replacement TUI has launched.
|
|
227
|
+
* Fresh sessions already connected their discovery listener before launch;
|
|
228
|
+
* this is therefore an idempotent no-op for them and for a healthy observer. */
|
|
229
|
+
startLiveCodexObserver(localThreadId: string): void;
|
|
230
|
+
/** Omnigent treats the forwarder as a required component of one native
|
|
231
|
+
* lifecycle: if its transport dies, it closes the app-server instead of
|
|
232
|
+
* accepting turns that can no longer reach the canonical mirror. */
|
|
233
|
+
private failObserverLifecycle;
|
|
222
234
|
private shouldIgnoreManagedForkThreadStarted;
|
|
223
235
|
private rememberManagedForkThreadStart;
|
|
224
236
|
/**
|
|
225
237
|
* Subscribe the forwarder connection to a thread (reference implementation's
|
|
226
238
|
* `_subscribe_until_ready`). A fresh TUI thread has no rollout until its first
|
|
227
239
|
* turn, so `thread/resume` is retried: park until the forwarder observes the
|
|
228
|
-
* thread active, then retry.
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
240
|
+
* thread active, then retry. Like Omnigent, the first attempt always uses
|
|
241
|
+
* `excludeTurns`: a known-session cold resume therefore never reconstructs
|
|
242
|
+
* historical running/completed state. Only a fresh thread whose first attempt
|
|
243
|
+
* failed as not-ready retries without `excludeTurns`, backfilling the newly
|
|
244
|
+
* materialized first turn and de-duplicating it against live notifications.
|
|
232
245
|
*/
|
|
233
246
|
private subscribeUntilReady;
|
|
234
|
-
/** Restore the independent observer after an unexpected exit. The active turn
|
|
235
|
-
* remains open during the bounded grace so resume can reconcile its exact id. */
|
|
236
|
-
private reconnectForwarder;
|
|
237
|
-
private armObserverReconnectDeadline;
|
|
238
|
-
private clearObserverReconnectDeadline;
|
|
239
247
|
/** Await a live session's thread binding. `null` leaves the deadline to the
|
|
240
248
|
* caller; a number keeps the Provider-local bound. Returns false on timeout /
|
|
241
249
|
* no live session. Injection and the runner's `live.ready` gate on this. */
|
|
242
250
|
waitLiveReady(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
|
|
243
|
-
/** Await the
|
|
244
|
-
*
|
|
245
|
-
*
|
|
251
|
+
/** Await the observer subscription diagnostic. Codex/Traex Terminal startup
|
|
252
|
+
* deliberately does not gate on this promise: the dedicated preload owns resume,
|
|
253
|
+
* while the independent observer attaches in the background. */
|
|
246
254
|
waitTerminalReady(localThreadId: string, timeoutMs?: number | null): Promise<boolean>;
|
|
247
255
|
/** Diagnostic from the provider adapter when native discovery/resume failed. */
|
|
248
256
|
liveSessionError(localThreadId: string): string | undefined;
|
|
257
|
+
/** Structured phase + concrete cause used by the runner wire protocol. */
|
|
258
|
+
liveSessionFailure(localThreadId: string): LiveSessionFailure | undefined;
|
|
249
259
|
/** Publish the background TUI/thread discovery failure so an executor
|
|
250
260
|
* already waiting in the 60s bridge window exits immediately with the exact
|
|
251
261
|
* 30s discovery cause. */
|
|
@@ -257,13 +267,13 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
257
267
|
* resumed from the persisted native id. Serialized per session so two
|
|
258
268
|
* injects can't double-open a turn.
|
|
259
269
|
*
|
|
260
|
-
* Returns an {@link
|
|
270
|
+
* Returns an {@link InjectResult}: `notLive` when this session has no live
|
|
261
271
|
* forwarder (caller may use the run path); `notReady`/`failed` are hard errors
|
|
262
272
|
* the caller reports WITHOUT re-running (re-running double-writes alongside the
|
|
263
273
|
* forwarder). Park-until-ready (~60s), aligning reference implementation's executor waiting for
|
|
264
274
|
* the bridge instead of a short race that falls back to a second output path.
|
|
265
275
|
*/
|
|
266
|
-
injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<
|
|
276
|
+
injectMessage(localThreadId: string, input: RuntimeUserInput | string): Promise<InjectResult>;
|
|
267
277
|
/**
|
|
268
278
|
* Interrupt the session's active turn — the web Stop button. codex: the
|
|
269
279
|
* app-server `turn/interrupt` on the active `{threadId, turnId}` (exactly what
|
|
@@ -277,6 +287,11 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
277
287
|
stopLiveCodexSession(localThreadId: string, opts?: {
|
|
278
288
|
deferClaudeInteractionCleanup?: boolean;
|
|
279
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;
|
|
280
295
|
/** Complete the second shutdown phase after the runner has killed all native
|
|
281
296
|
* terminals and hook subprocesses. Must run before the runner process exits. */
|
|
282
297
|
finalizeStoppedLiveSessions(): void;
|
|
@@ -298,7 +313,7 @@ export declare class LocalAgentHost implements CodexCapabilities {
|
|
|
298
313
|
* serialized per session. Parks until the thread is ready AND the tmux injector
|
|
299
314
|
* is (re)attached, then pastes; `injectViaTerminal` RAISES if the prompt never
|
|
300
315
|
* appears (reference implementation RAISE), so a not-ready pane is a hard error — NOT a
|
|
301
|
-
* fall-through-to-run signal. Returns {@link
|
|
316
|
+
* fall-through-to-run signal. Returns {@link InjectResult}. */
|
|
302
317
|
private injectClaude;
|
|
303
318
|
/** Park until the claude session's tmux injector is (re)attached by the
|
|
304
319
|
* runner-child, or the deadline passes. Pane relaunch re-attaches it via
|
|
@@ -339,7 +354,3 @@ export declare function isUnsupportedMethodError(error: unknown): boolean;
|
|
|
339
354
|
* — a fresh TUI thread before its first turn. Retryable (park until active).
|
|
340
355
|
* Mirrors reference implementation's `_is_thread_not_ready_error`. */
|
|
341
356
|
export declare function isThreadNotReadyError(error: unknown): boolean;
|
|
342
|
-
/** A persisted thread id that a freshly started app-server cannot load yet.
|
|
343
|
-
* During startup, both errors can be transient while the rollout index catches
|
|
344
|
-
* up. Retry the same id; never use either error as permission to replace it. */
|
|
345
|
-
export declare function isRetryableThreadResumeError(error: unknown): boolean;
|