@rynx-ai/runtime 0.1.11-beta.3 → 0.1.11-beta.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude/executor.d.ts +19 -5
- package/dist/claude/executor.js +56 -12
- package/dist/claude/models.d.ts +0 -5
- package/dist/claude/models.js +1 -7
- package/dist/claude/native-bridge.d.ts +2 -0
- package/dist/claude/native-bridge.js +23 -0
- package/dist/claude/native-hook-main.js +62 -0
- package/dist/claude/native-integration.d.ts +50 -10
- package/dist/claude/native-integration.js +262 -37
- package/dist/claude/session-status.d.ts +39 -0
- package/dist/claude/session-status.js +163 -0
- package/dist/claude/transcript.js +27 -17
- package/dist/codex-app-server/client.d.ts +10 -6
- package/dist/codex-app-server/client.js +67 -15
- package/dist/codex-app-server/forwarder.d.ts +92 -3
- package/dist/codex-app-server/forwarder.js +509 -56
- package/dist/codex-app-server/mapping.d.ts +3 -6
- package/dist/codex-app-server/mapping.js +174 -28
- package/dist/codex-app-server/mcp-startup.d.ts +13 -0
- package/dist/codex-app-server/mcp-startup.js +63 -0
- package/dist/codex-app-server/protocol.d.ts +64 -7
- package/dist/codex-app-server/ws-channel.js +19 -19
- package/dist/codex-home.js +2 -4
- package/dist/host.d.ts +64 -21
- package/dist/host.js +1330 -441
- package/dist/index.d.ts +1 -1
- package/dist/input-resources.d.ts +4 -0
- package/dist/input-resources.js +21 -5
- package/dist/models-catalog.d.ts +2 -1
- package/dist/models-catalog.js +94 -6
- package/dist/runner/child.d.ts +48 -21
- package/dist/runner/child.js +550 -48
- package/dist/runner/manager.d.ts +54 -13
- package/dist/runner/manager.js +479 -114
- package/dist/runner/protocol.d.ts +62 -19
- package/dist/runner/protocol.js +5 -0
- package/dist/runner/startup-policy.d.ts +7 -0
- package/dist/runner/startup-policy.js +10 -0
- package/dist/terminal/claude-tui.d.ts +3 -1
- package/dist/terminal/claude-tui.js +3 -1
- package/dist/terminal/registry.js +3 -2
- package/dist/terminal/tmux.d.ts +50 -7
- package/dist/terminal/tmux.js +168 -47
- package/package.json +4 -3
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const STATUS_TO_RUNNER = {
|
|
5
|
+
busy: "running",
|
|
6
|
+
waiting: "running",
|
|
7
|
+
idle: "idle",
|
|
8
|
+
shell: "idle",
|
|
9
|
+
running: "running",
|
|
10
|
+
completed: "idle",
|
|
11
|
+
failed: "idle",
|
|
12
|
+
error: "idle",
|
|
13
|
+
done: "idle",
|
|
14
|
+
};
|
|
15
|
+
const SCAN_FRESHNESS_MS = 120_000;
|
|
16
|
+
const DEFAULT_MAX_RESOLVE_ATTEMPTS = 40;
|
|
17
|
+
function isRecord(value) {
|
|
18
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
function readJsonRecord(path) {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
23
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function matchesSession(record, expectedSessionId) {
|
|
30
|
+
if (record.kind !== "interactive")
|
|
31
|
+
return false;
|
|
32
|
+
return expectedSessionId === undefined || record.sessionId === expectedSessionId;
|
|
33
|
+
}
|
|
34
|
+
export function claudeSessionsDir(configDir = process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude")) {
|
|
35
|
+
return join(configDir, "sessions");
|
|
36
|
+
}
|
|
37
|
+
export function resolveClaudeSessionStatusFile({ panePid, expectedSessionId, configDir, now = Date.now(), }) {
|
|
38
|
+
const directory = claudeSessionsDir(configDir);
|
|
39
|
+
if (panePid !== undefined) {
|
|
40
|
+
const candidate = join(directory, `${panePid}.json`);
|
|
41
|
+
const record = readJsonRecord(candidate);
|
|
42
|
+
if (record && matchesSession(record, expectedSessionId))
|
|
43
|
+
return candidate;
|
|
44
|
+
}
|
|
45
|
+
if (!expectedSessionId)
|
|
46
|
+
return undefined;
|
|
47
|
+
try {
|
|
48
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
49
|
+
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
|
50
|
+
continue;
|
|
51
|
+
const candidate = join(directory, entry.name);
|
|
52
|
+
let modifiedAt;
|
|
53
|
+
try {
|
|
54
|
+
modifiedAt = statSync(candidate).mtimeMs;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (now - modifiedAt > SCAN_FRESHNESS_MS)
|
|
60
|
+
continue;
|
|
61
|
+
const record = readJsonRecord(candidate);
|
|
62
|
+
if (record && matchesSession(record, expectedSessionId))
|
|
63
|
+
return candidate;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
export function readClaudeSessionStatus(path) {
|
|
72
|
+
const record = readJsonRecord(path);
|
|
73
|
+
if (!record || typeof record.status !== "string")
|
|
74
|
+
return undefined;
|
|
75
|
+
const runnerStatus = STATUS_TO_RUNNER[record.status];
|
|
76
|
+
if (!runnerStatus)
|
|
77
|
+
return undefined;
|
|
78
|
+
const updatedAt = record.statusUpdatedAt;
|
|
79
|
+
const waitingFor = record.status === "waiting" ? record.waitingFor : undefined;
|
|
80
|
+
return {
|
|
81
|
+
runnerStatus,
|
|
82
|
+
rawStatus: record.status,
|
|
83
|
+
...(Number.isSafeInteger(updatedAt) ? { statusUpdatedAt: updatedAt } : {}),
|
|
84
|
+
...(typeof waitingFor === "string" && waitingFor ? { blockedOn: waitingFor } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export class ClaudeSessionStatusPoller {
|
|
88
|
+
options;
|
|
89
|
+
path;
|
|
90
|
+
attempts = 0;
|
|
91
|
+
exhausted = false;
|
|
92
|
+
lastMtime;
|
|
93
|
+
lastEdge;
|
|
94
|
+
lastStatus;
|
|
95
|
+
constructor(options) {
|
|
96
|
+
this.options = options;
|
|
97
|
+
}
|
|
98
|
+
get active() {
|
|
99
|
+
return this.path !== undefined && !this.exhausted;
|
|
100
|
+
}
|
|
101
|
+
get status() {
|
|
102
|
+
return this.lastStatus;
|
|
103
|
+
}
|
|
104
|
+
tick() {
|
|
105
|
+
if (this.exhausted)
|
|
106
|
+
return;
|
|
107
|
+
if (!this.path) {
|
|
108
|
+
this.tryResolve();
|
|
109
|
+
if (!this.path)
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.readAndPublish();
|
|
113
|
+
}
|
|
114
|
+
retire() {
|
|
115
|
+
this.exhausted = true;
|
|
116
|
+
}
|
|
117
|
+
resync() {
|
|
118
|
+
if (!this.active)
|
|
119
|
+
return;
|
|
120
|
+
this.lastMtime = undefined;
|
|
121
|
+
this.lastEdge = undefined;
|
|
122
|
+
}
|
|
123
|
+
tryResolve() {
|
|
124
|
+
this.attempts += 1;
|
|
125
|
+
this.path = resolveClaudeSessionStatusFile({
|
|
126
|
+
panePid: this.options.panePid(),
|
|
127
|
+
expectedSessionId: this.options.sessionId(),
|
|
128
|
+
...(this.options.configDir ? { configDir: this.options.configDir } : {}),
|
|
129
|
+
now: this.options.now?.() ?? Date.now(),
|
|
130
|
+
});
|
|
131
|
+
if (!this.path &&
|
|
132
|
+
this.attempts >= (this.options.maxResolveAttempts ?? DEFAULT_MAX_RESOLVE_ATTEMPTS)) {
|
|
133
|
+
this.exhausted = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
readAndPublish() {
|
|
137
|
+
if (!this.path)
|
|
138
|
+
return;
|
|
139
|
+
let mtime;
|
|
140
|
+
try {
|
|
141
|
+
mtime = statSync(this.path).mtimeMs;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
this.exhausted = true;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (this.lastMtime === mtime)
|
|
148
|
+
return;
|
|
149
|
+
this.lastMtime = mtime;
|
|
150
|
+
const status = readClaudeSessionStatus(this.path);
|
|
151
|
+
if (!status) {
|
|
152
|
+
this.lastStatus = undefined;
|
|
153
|
+
this.lastEdge = undefined;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this.lastStatus = status;
|
|
157
|
+
const edge = `${status.runnerStatus}\0${status.blockedOn ?? ""}`;
|
|
158
|
+
if (edge === this.lastEdge)
|
|
159
|
+
return;
|
|
160
|
+
this.lastEdge = edge;
|
|
161
|
+
this.options.onStatus(status);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -22,11 +22,32 @@ export function subagentTranscriptPath(parentTranscriptPath, agentId) {
|
|
|
22
22
|
const dir = parentTranscriptPath.replace(/\.jsonl$/, "");
|
|
23
23
|
return join(dir, "subagents", `agent-${agentId}.jsonl`);
|
|
24
24
|
}
|
|
25
|
+
function strippedImagePlaceholder(source) {
|
|
26
|
+
const mediaType = source.media_type;
|
|
27
|
+
const label = typeof mediaType === "string" && mediaType ? `${mediaType} image` : "image";
|
|
28
|
+
return `[${label} omitted from history to save context — re-run the tool call above (e.g. Read the same path) to view it again]`;
|
|
29
|
+
}
|
|
30
|
+
/** Remove Claude's inline image bytes before tool output reaches canonical
|
|
31
|
+
* history. A Read image result can contain a full-resolution base64 payload;
|
|
32
|
+
* replaying it as text only bloats the transcript, while the model cannot use
|
|
33
|
+
* those encoded bytes as text. Keep a small, human-readable marker instead. */
|
|
34
|
+
function stripInlineImageData(value) {
|
|
35
|
+
if (Array.isArray(value))
|
|
36
|
+
return value.map(stripInlineImageData);
|
|
37
|
+
if (!isObject(value))
|
|
38
|
+
return value;
|
|
39
|
+
const source = isObject(value.source) ? value.source : undefined;
|
|
40
|
+
if (value.type === "image" && source) {
|
|
41
|
+
return { type: "text", text: strippedImagePlaceholder(source) };
|
|
42
|
+
}
|
|
43
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, stripInlineImageData(nested)]));
|
|
44
|
+
}
|
|
25
45
|
function stringifyToolContent(content) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
const stripped = stripInlineImageData(content);
|
|
47
|
+
if (typeof stripped === "string")
|
|
48
|
+
return stripped;
|
|
49
|
+
if (Array.isArray(stripped)) {
|
|
50
|
+
return stripped
|
|
30
51
|
.map((part) => part && typeof part === "object" && "text" in part
|
|
31
52
|
? String(part.text ?? "")
|
|
32
53
|
: typeof part === "string"
|
|
@@ -34,18 +55,7 @@ function stringifyToolContent(content) {
|
|
|
34
55
|
: JSON.stringify(part))
|
|
35
56
|
.join("");
|
|
36
57
|
}
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
function toolLabel(name, input) {
|
|
40
|
-
if (input && typeof input === "object") {
|
|
41
|
-
const rec = input;
|
|
42
|
-
for (const key of ["command", "cmd", "path", "file_path", "pattern", "query"]) {
|
|
43
|
-
const v = rec[key];
|
|
44
|
-
if (typeof v === "string" && v)
|
|
45
|
-
return v;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return name ?? "tool";
|
|
58
|
+
return stripped == null ? "" : JSON.stringify(stripped);
|
|
49
59
|
}
|
|
50
60
|
const BASH_INPUT_RE = /<bash-input>([\s\S]*?)<\/bash-input>/;
|
|
51
61
|
const BASH_STDOUT_RE = /<bash-stdout>([\s\S]*?)<\/bash-stdout>/;
|
|
@@ -132,7 +142,7 @@ export function parseTranscriptRecord(record, opts) {
|
|
|
132
142
|
type: "tool",
|
|
133
143
|
event: "on_tool_start",
|
|
134
144
|
name: block.name,
|
|
135
|
-
input: { ...(isObject(block.input) ? block.input : {}), id: block.id
|
|
145
|
+
input: { ...(isObject(block.input) ? block.input : {}), id: block.id },
|
|
136
146
|
data: { id: block.id },
|
|
137
147
|
...parentTag,
|
|
138
148
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RuntimeUserInput, SessionInteractionResolution } from "@rynx-ai/core";
|
|
2
|
-
import type { AskForApproval, ClientInfo, CollaborationModeListResponse, GetAuthStatusParams, GetAuthStatusResponse, InitializeResponse, ModelListParams, ModelListResponse, ReviewStartParams, ReviewStartResponse, SandboxMode, ThreadForkParams, ThreadGoalClearParams, ThreadGoalGetParams, ThreadGoalGetResponse, ThreadGoalSetParams, ThreadListParams, ThreadListResponse, ResumedThread, ThreadResumeParams, ThreadSettingsUpdateParams, ThreadStartParams, TurnInterruptParams, TurnStartParams, TurnSteerParams, UserInput } from "./protocol.js";
|
|
2
|
+
import type { AskForApproval, ClientInfo, CollaborationModeListResponse, ConfigReadParams, ConfigReadResponse, GetAuthStatusParams, GetAuthStatusResponse, InitializeResponse, ModelListParams, ModelListResponse, ReviewStartParams, ReviewStartResponse, SandboxMode, ThreadForkParams, ThreadGoalClearParams, ThreadGoalGetParams, ThreadGoalGetResponse, ThreadGoalSetParams, ThreadListParams, ThreadListResponse, ResumedThread, ThreadResumeParams, ThreadRuntimeSettings, ThreadSettingsUpdateParams, ThreadStartParams, TurnInterruptParams, TurnStartParams, TurnSteerParams, UserInput } from "./protocol.js";
|
|
3
3
|
import { CodexAppServerTransport, type CodexAppServerProcessSpawner, type RpcChannel, type TransportLogger } from "./transport.js";
|
|
4
4
|
import type { ResolveInteractionResult, RuntimeInteractionListener } from "../interactions.js";
|
|
5
5
|
export type ApprovalDecisionPolicy = "auto-approve-session" | "auto-decline" | "auto-cancel";
|
|
@@ -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
|
|
@@ -42,13 +44,16 @@ export declare class CodexAppServerClient {
|
|
|
42
44
|
terminalRemoteUrl(): string | undefined;
|
|
43
45
|
ensureInitialized(): Promise<InitializeResponse>;
|
|
44
46
|
getAuthStatus(params?: GetAuthStatusParams): Promise<GetAuthStatusResponse>;
|
|
47
|
+
/** Read the runtime's merged effective config. This is the stable seam for
|
|
48
|
+
* discovering a fresh thread's model across Traex YAML/TOML generations. */
|
|
49
|
+
configRead(params: ConfigReadParams): Promise<ConfigReadResponse>;
|
|
45
50
|
threadStart(params: ThreadStartParams): Promise<{
|
|
46
51
|
threadId: string;
|
|
47
|
-
}>;
|
|
52
|
+
} & ThreadRuntimeSettings>;
|
|
48
53
|
threadResume(params: ThreadResumeParams): Promise<{
|
|
49
54
|
threadId: string;
|
|
50
55
|
thread: ResumedThread;
|
|
51
|
-
}>;
|
|
56
|
+
} & ThreadRuntimeSettings>;
|
|
52
57
|
turnStart(params: TurnStartParams): Promise<{
|
|
53
58
|
turnId: string;
|
|
54
59
|
}>;
|
|
@@ -94,9 +99,8 @@ export declare class CodexAppServerClient {
|
|
|
94
99
|
* resolver still wins correctly.
|
|
95
100
|
*/
|
|
96
101
|
setInteractionListener(listener: RuntimeInteractionListener | null): void;
|
|
97
|
-
/** Observe the
|
|
98
|
-
*
|
|
99
|
-
* native request still has to count as unavailable during host failover. */
|
|
102
|
+
/** Observe the initialized connection lifecycle. Registration never reports
|
|
103
|
+
* disconnected for a client that has not connected yet. */
|
|
100
104
|
setConnectionListener(listener: ((state: "connected" | "disconnected") => void) | null): void;
|
|
101
105
|
private setConnectionState;
|
|
102
106
|
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,30 +990,66 @@ 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();
|
|
1002
1021
|
return this.transport.sendRequest("getAuthStatus", params);
|
|
1003
1022
|
}
|
|
1023
|
+
/** Read the runtime's merged effective config. This is the stable seam for
|
|
1024
|
+
* discovering a fresh thread's model across Traex YAML/TOML generations. */
|
|
1025
|
+
async configRead(params) {
|
|
1026
|
+
await this.ensureInitialized();
|
|
1027
|
+
return this.transport.sendRequest("config/read", params);
|
|
1028
|
+
}
|
|
1004
1029
|
async threadStart(params) {
|
|
1005
1030
|
await this.ensureInitialized();
|
|
1006
1031
|
const response = await this.transport.sendRequest("thread/start", params);
|
|
1007
|
-
return {
|
|
1032
|
+
return {
|
|
1033
|
+
threadId: response.thread.id,
|
|
1034
|
+
...(response.model ? { model: response.model } : {}),
|
|
1035
|
+
...(response.reasoningEffort === undefined
|
|
1036
|
+
? {}
|
|
1037
|
+
: { reasoningEffort: response.reasoningEffort }),
|
|
1038
|
+
};
|
|
1008
1039
|
}
|
|
1009
1040
|
async threadResume(params) {
|
|
1010
1041
|
await this.ensureInitialized();
|
|
1011
1042
|
const response = await this.transport.sendRequest("thread/resume", params);
|
|
1012
1043
|
// Expose the whole `thread` (not just its id): its `turns[].items[]` are the
|
|
1013
1044
|
// backfill the forwarder replays for a fresh thread's first turn.
|
|
1014
|
-
return {
|
|
1045
|
+
return {
|
|
1046
|
+
threadId: response.thread.id,
|
|
1047
|
+
thread: response.thread,
|
|
1048
|
+
...(response.model ? { model: response.model } : {}),
|
|
1049
|
+
...(response.reasoningEffort === undefined
|
|
1050
|
+
? {}
|
|
1051
|
+
: { reasoningEffort: response.reasoningEffort }),
|
|
1052
|
+
};
|
|
1015
1053
|
}
|
|
1016
1054
|
async turnStart(params) {
|
|
1017
1055
|
await this.ensureInitialized();
|
|
@@ -1146,14 +1184,18 @@ export class CodexAppServerClient {
|
|
|
1146
1184
|
setInteractionListener(listener) {
|
|
1147
1185
|
this.interactionListener = listener;
|
|
1148
1186
|
}
|
|
1149
|
-
/** Observe the
|
|
1150
|
-
*
|
|
1151
|
-
* native request still has to count as unavailable during host failover. */
|
|
1187
|
+
/** Observe the initialized connection lifecycle. Registration never reports
|
|
1188
|
+
* disconnected for a client that has not connected yet. */
|
|
1152
1189
|
setConnectionListener(listener) {
|
|
1153
1190
|
this.connectionListener = listener;
|
|
1154
|
-
listener
|
|
1191
|
+
if (listener && this.connectionEstablished)
|
|
1192
|
+
listener(this.connectionState);
|
|
1155
1193
|
}
|
|
1156
1194
|
setConnectionState(state) {
|
|
1195
|
+
if (state === "connected")
|
|
1196
|
+
this.connectionEstablished = true;
|
|
1197
|
+
if (state === "disconnected" && !this.connectionEstablished)
|
|
1198
|
+
return;
|
|
1157
1199
|
if (this.connectionState === state)
|
|
1158
1200
|
return;
|
|
1159
1201
|
this.connectionState = state;
|
|
@@ -1363,7 +1405,17 @@ export function buildTextUserInput(message) {
|
|
|
1363
1405
|
export function buildRuntimeUserInput(input) {
|
|
1364
1406
|
return input.content.map((part) => part.type === "text"
|
|
1365
1407
|
? { type: "text", text: part.text, text_elements: [] }
|
|
1366
|
-
:
|
|
1408
|
+
: part.type === "local_image"
|
|
1409
|
+
? { type: "localImage", path: part.path }
|
|
1410
|
+
: {
|
|
1411
|
+
type: "text",
|
|
1412
|
+
text: `[[RYNX_FILE_RESOURCE ${JSON.stringify({
|
|
1413
|
+
path: part.path,
|
|
1414
|
+
...(part.resource.filename ? { filename: part.resource.filename } : {}),
|
|
1415
|
+
mediaType: part.resource.mediaType,
|
|
1416
|
+
})}]]\nInspect this absolute file path with the available file-reading tools before answering.`,
|
|
1417
|
+
text_elements: [],
|
|
1418
|
+
});
|
|
1367
1419
|
}
|
|
1368
1420
|
const defaultLogger = {
|
|
1369
1421
|
log(entry) {
|
|
@@ -23,20 +23,37 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import type { AgentEvent, UserContentPart } from "@rynx-ai/core";
|
|
25
25
|
import type { CodexAppServerClient } from "./client.js";
|
|
26
|
-
import type {
|
|
26
|
+
import type { McpStartupPlan } from "./mcp-startup.js";
|
|
27
|
+
import type { CollaborationModeKind, ReasoningEffort, ResumedTurn } from "./protocol.js";
|
|
27
28
|
export interface CodexForwarderSink {
|
|
28
29
|
/** A turn began. `turnId` is codex's turn id, used to derive a stable
|
|
29
30
|
* `responseId`. Start a fresh normalizer/response. */
|
|
30
31
|
onTurnStart(turnId?: string): void;
|
|
32
|
+
/** The observer received the provider's authoritative `turn/started` edge.
|
|
33
|
+
* Unlike `onTurnStart`, this is not fired early by turn/start acceptance. */
|
|
34
|
+
onTurnObserved?(turnId?: string): void;
|
|
31
35
|
/** One mapped event within the current turn. */
|
|
32
36
|
onEvent(event: AgentEvent): void;
|
|
37
|
+
/** Turn-scoped content arrived while no Provider Turn is active. Attach it
|
|
38
|
+
* to that response without opening a Turn or changing Session status. */
|
|
39
|
+
onTurnContentEvent?(turnId: string | undefined, event: AgentEvent): void;
|
|
40
|
+
/** Provider startup is session status, not a model item. Hosts that already
|
|
41
|
+
* published the response can forward it without synthesizing another start. */
|
|
42
|
+
onStatus?(note: string | undefined, statusKind?: "startup"): void;
|
|
33
43
|
/** The current turn finished; `usage` is the runtime's raw snapshot if any. */
|
|
34
|
-
onTurnEnd(usage?: Record<string, unknown
|
|
44
|
+
onTurnEnd(usage?: Record<string, unknown>, reason?: "superseded"): void;
|
|
45
|
+
/** The provider confirmed that the active turn was explicitly interrupted. */
|
|
46
|
+
onTurnInterrupted?(usage?: Record<string, unknown>): void;
|
|
47
|
+
/** Resume proved that the newest turn is terminal even though its live edge
|
|
48
|
+
* was missed. This updates session state without replaying historical items. */
|
|
49
|
+
onRecoveredTurnStatus?(status: "idle" | "failed", turnId: string | undefined, error?: Error): void;
|
|
35
50
|
/** A turn failed on the runtime. */
|
|
36
51
|
onTurnError(error: Error): void;
|
|
37
52
|
/** The user's turn text (sourced from codex's `userMessage` item), so a
|
|
38
53
|
* co-driving TUI's prompt is recorded even though this process never injected it. */
|
|
39
54
|
onUserMessage?(content: string | UserContentPart[]): void;
|
|
55
|
+
/** Out-of-lifecycle counterpart of `onUserMessage`, scoped when possible. */
|
|
56
|
+
onTurnContentUserMessage?(turnId: string | undefined, content: string | UserContentPart[]): void;
|
|
40
57
|
/** A managed Core fork also broadcasts `thread/started`, but does not switch
|
|
41
58
|
* the source TUI. Discard that notification before changing the bound thread. */
|
|
42
59
|
shouldIgnoreThreadStarted?(threadId: string, forkedFromId?: string): boolean;
|
|
@@ -46,6 +63,22 @@ export interface CodexForwarderSink {
|
|
|
46
63
|
/** The thread showed activity (a turn/item began, so its rollout now exists).
|
|
47
64
|
* Fired once; lets a parked `thread/resume` retry (reference implementation's ready signal). */
|
|
48
65
|
onThreadActive?(): void;
|
|
66
|
+
/** The native TUI or another app-server client changed collaboration mode. */
|
|
67
|
+
onCollaborationModeChanged?(mode: CollaborationModeKind): void;
|
|
68
|
+
/** Full mutable settings reported by the native thread. In particular, a
|
|
69
|
+
* TUI `/model` switch must become the model used by the next mode snapshot. */
|
|
70
|
+
onThreadSettingsChanged?(settings: {
|
|
71
|
+
model?: string;
|
|
72
|
+
reasoningEffort?: ReasoningEffort | null;
|
|
73
|
+
}): void;
|
|
74
|
+
/** Codex's terminal-local Plan picker is not emitted by app-server today.
|
|
75
|
+
* Synthesize it only after a live Plan item and its Turn both complete. */
|
|
76
|
+
onPlanImplementationPrompt?(prompt: CodexPlanImplementationPrompt): void;
|
|
77
|
+
}
|
|
78
|
+
export interface CodexPlanImplementationPrompt {
|
|
79
|
+
threadId: string;
|
|
80
|
+
turnId: string;
|
|
81
|
+
text: string;
|
|
49
82
|
}
|
|
50
83
|
export interface CodexSessionForwarderOptions {
|
|
51
84
|
/** Some Codex-lineage runtimes publish the final item one frame after
|
|
@@ -58,6 +91,9 @@ export interface CodexSessionForwarderOptions {
|
|
|
58
91
|
assistantMessageGraceMs?: number;
|
|
59
92
|
/** Surface Traex's provider-capacity queue as a canonical running status. */
|
|
60
93
|
surfaceQueueStatus?: boolean;
|
|
94
|
+
/** Provider-configured MCP servers. Their startup round is synthesized because
|
|
95
|
+
* Codex currently sends per-server edges only to the thread-owning TUI. */
|
|
96
|
+
mcpStartup?: McpStartupPlan | null;
|
|
61
97
|
}
|
|
62
98
|
export declare class CodexSessionForwarder {
|
|
63
99
|
private readonly client;
|
|
@@ -69,9 +105,16 @@ export declare class CodexSessionForwarder {
|
|
|
69
105
|
private currentThreadIdValue;
|
|
70
106
|
private activeSignaled;
|
|
71
107
|
private completionTimer;
|
|
108
|
+
/** Turn id retained only for late-item dedup while a terminal response waits
|
|
109
|
+
* for its bounded output-ordering grace. It is not an active provider turn. */
|
|
110
|
+
private pendingCompletionTurnId;
|
|
72
111
|
private assistantMessageTimer;
|
|
73
112
|
private deferredAssistantMessage;
|
|
74
113
|
private pendingCompletion;
|
|
114
|
+
private readonly pendingMcpServers;
|
|
115
|
+
private readonly failedMcpServers;
|
|
116
|
+
private mcpStartupTimer;
|
|
117
|
+
private lastMcpStatusNote;
|
|
75
118
|
/** Completed-item dedup keys already mirrored (live vs resume backfill). Key =
|
|
76
119
|
* `threadId:turnId:item.id`; anonymous items use a per-(thread,turn) position
|
|
77
120
|
* counter. Mirrors reference implementation `_completed_item_key` + `synced_item_keys`. */
|
|
@@ -79,16 +122,38 @@ export declare class CodexSessionForwarder {
|
|
|
79
122
|
/** Per-(thread,turn) position counter for items lacking a stable codex id
|
|
80
123
|
* (peek-then-advance; advanced only on a successful claim). reference implementation anon path. */
|
|
81
124
|
private readonly anonCounters;
|
|
125
|
+
private pendingPlanImplementation;
|
|
126
|
+
/** Latest aggregate diff per Turn. Codex republishes the complete diff after
|
|
127
|
+
* every edit; only the terminal snapshot belongs in transcript history. */
|
|
128
|
+
private readonly turnDiffByTurn;
|
|
129
|
+
private replayingBackfill;
|
|
82
130
|
constructor(client: CodexAppServerClient, sink: CodexForwarderSink, options?: CodexSessionForwarderOptions);
|
|
83
131
|
/** Begin mirroring. Idempotent. */
|
|
84
132
|
start(): void;
|
|
85
133
|
stop(): void;
|
|
86
|
-
/** True while
|
|
134
|
+
/** True while the provider owns an active turn, including the short interval
|
|
135
|
+
* between injection acceptance and observer confirmation. */
|
|
87
136
|
isTurnOpen(): boolean;
|
|
88
137
|
/** The current turn's codex id (only meaningful while {@link isTurnOpen}). */
|
|
89
138
|
currentTurnId(): string | null;
|
|
139
|
+
/**
|
|
140
|
+
* Record a turn accepted by the injection connection before the independent
|
|
141
|
+
* observer receives `turn/started`. This closes the read-decide-RPC-write race:
|
|
142
|
+
* another web message arriving in that window must steer this turn, not start
|
|
143
|
+
* a second one.
|
|
144
|
+
*/
|
|
145
|
+
noteTurnAccepted(turnId: string): void;
|
|
146
|
+
hasPendingMcpStartup(): boolean;
|
|
147
|
+
/** Mark and return the startup servers cancelled by a web Stop. */
|
|
148
|
+
cancelMcpStartup(): string[];
|
|
149
|
+
/** Diagnostic suffix for an injection failure during Provider startup. */
|
|
150
|
+
mcpStartupDetail(): string | null;
|
|
90
151
|
/** The bound codex thread id captured from `thread/started` (null until then). */
|
|
91
152
|
threadId(): string | null;
|
|
153
|
+
/** Seed an already-persisted/resumed thread binding. The bridge retains this
|
|
154
|
+
* state even when app-server does not rebroadcast `thread/started`, so
|
|
155
|
+
* terminal-boundary recovery must know it too. */
|
|
156
|
+
noteThreadBound(threadId: string): void;
|
|
92
157
|
/**
|
|
93
158
|
* Replay the backlog turns from a `thread/resume` response as if they were live
|
|
94
159
|
* `item/completed` notifications — the fresh-thread first-turn backfill. Each
|
|
@@ -97,11 +162,21 @@ export declare class CodexSessionForwarder {
|
|
|
97
162
|
* not doubled.
|
|
98
163
|
*/
|
|
99
164
|
replayBackfill(turns: ResumedTurn[]): void;
|
|
165
|
+
/** Suppress our synthesized picker when a future app-server emits the native
|
|
166
|
+
* `plan_implementation` request itself. */
|
|
167
|
+
noteNativePlanImplementationPrompt(turnId?: string): void;
|
|
168
|
+
/** Fail an open response exactly once when its observer or terminal exits. */
|
|
169
|
+
failOpenTurn(error: Error): boolean;
|
|
100
170
|
private handle;
|
|
101
171
|
private scheduleCompletion;
|
|
102
172
|
private refreshCompletionGrace;
|
|
103
173
|
private flushPendingCompletion;
|
|
104
174
|
private settle;
|
|
175
|
+
private handleMcpStartupStatus;
|
|
176
|
+
private settleMcpStartup;
|
|
177
|
+
private clearMcpStartupTimer;
|
|
178
|
+
private emitMcpStartupStatus;
|
|
179
|
+
private emitMcpStatus;
|
|
105
180
|
/** Map + emit one completed codex item, deduped by a TOTAL key and routing the
|
|
106
181
|
* user echo to {@link CodexForwarderSink.onUserMessage}. Shared by live + backfill. */
|
|
107
182
|
private processCompletedItem;
|
|
@@ -118,4 +193,18 @@ export declare class CodexSessionForwarder {
|
|
|
118
193
|
private completedItemKey;
|
|
119
194
|
private advanceAnonCounter;
|
|
120
195
|
private ensureTurn;
|
|
196
|
+
private consumeTurnDiff;
|
|
197
|
+
/** Start (or confirm) the app-server's authoritative active turn. A newer
|
|
198
|
+
* start supersedes an older response whose terminal edge arrived late; a
|
|
199
|
+
* pending Traex completion is flushed first so its final item grace remains
|
|
200
|
+
* intact. */
|
|
201
|
+
private beginTurn;
|
|
202
|
+
/** Active-turn clearing contract:
|
|
203
|
+
*
|
|
204
|
+
* - an identified active turn is closed only by the same id;
|
|
205
|
+
* - an id-less boundary cannot close an identified active turn;
|
|
206
|
+
* - with no observed active turn, an identified boundary may recover a
|
|
207
|
+
* missed start only when it carries the currently-bound thread id. */
|
|
208
|
+
private terminalBoundaryMatchesActiveTurn;
|
|
209
|
+
private notificationMatchesCurrentThread;
|
|
121
210
|
}
|