@rynx-ai/runtime 0.1.11-beta.2 → 0.1.11-beta.21
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/models.d.ts +0 -5
- package/dist/claude/models.js +1 -7
- package/dist/claude/native-integration.d.ts +35 -7
- package/dist/claude/native-integration.js +204 -32
- package/dist/claude/session-status.d.ts +39 -0
- package/dist/claude/session-status.js +163 -0
- package/dist/codex-app-server/forwarder.d.ts +66 -2
- package/dist/codex-app-server/forwarder.js +329 -11
- package/dist/codex-app-server/mapping.d.ts +2 -0
- package/dist/codex-app-server/mapping.js +97 -23
- 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 +8 -5
- package/dist/codex-app-server/ws-channel.js +19 -19
- package/dist/codex-home.js +2 -4
- package/dist/host.d.ts +28 -7
- package/dist/host.js +356 -92
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/models-catalog.d.ts +2 -1
- package/dist/models-catalog.js +82 -3
- package/dist/runner/child.d.ts +22 -6
- package/dist/runner/child.js +237 -30
- package/dist/runner/manager.d.ts +87 -4
- package/dist/runner/manager.js +610 -56
- package/dist/runner/protocol.d.ts +11 -15
- package/dist/runner/startup-policy.d.ts +4 -0
- package/dist/runner/startup-policy.js +5 -0
- package/dist/terminal/claude-tui.d.ts +3 -1
- package/dist/terminal/claude-tui.js +3 -1
- package/dist/terminal/tmux.d.ts +29 -2
- package/dist/terminal/tmux.js +122 -14
- 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
|
+
}
|
|
@@ -23,15 +23,25 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import type { AgentEvent, UserContentPart } from "@rynx-ai/core";
|
|
25
25
|
import type { CodexAppServerClient } from "./client.js";
|
|
26
|
+
import type { McpStartupPlan } from "./mcp-startup.js";
|
|
26
27
|
import type { 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
|
+
/** Provider startup is session status, not a model item. Hosts that already
|
|
38
|
+
* published the response can forward it without synthesizing another start. */
|
|
39
|
+
onStatus?(note: string | undefined, statusKind?: "startup"): void;
|
|
33
40
|
/** The current turn finished; `usage` is the runtime's raw snapshot if any. */
|
|
34
|
-
onTurnEnd(usage?: Record<string, unknown
|
|
41
|
+
onTurnEnd(usage?: Record<string, unknown>, reason?: "superseded"): void;
|
|
42
|
+
/** Resume proved that the newest turn is terminal even though its live edge
|
|
43
|
+
* was missed. This updates session state without replaying historical items. */
|
|
44
|
+
onRecoveredTurnStatus?(status: "idle" | "failed", turnId: string | undefined, error?: Error): void;
|
|
35
45
|
/** A turn failed on the runtime. */
|
|
36
46
|
onTurnError(error: Error): void;
|
|
37
47
|
/** The user's turn text (sourced from codex's `userMessage` item), so a
|
|
@@ -58,6 +68,9 @@ export interface CodexSessionForwarderOptions {
|
|
|
58
68
|
assistantMessageGraceMs?: number;
|
|
59
69
|
/** Surface Traex's provider-capacity queue as a canonical running status. */
|
|
60
70
|
surfaceQueueStatus?: boolean;
|
|
71
|
+
/** Provider-configured MCP servers. Their startup round is synthesized because
|
|
72
|
+
* Codex currently sends per-server edges only to the thread-owning TUI. */
|
|
73
|
+
mcpStartup?: McpStartupPlan | null;
|
|
61
74
|
}
|
|
62
75
|
export declare class CodexSessionForwarder {
|
|
63
76
|
private readonly client;
|
|
@@ -69,9 +82,16 @@ export declare class CodexSessionForwarder {
|
|
|
69
82
|
private currentThreadIdValue;
|
|
70
83
|
private activeSignaled;
|
|
71
84
|
private completionTimer;
|
|
85
|
+
/** Turn id retained only for late-item dedup while a terminal response waits
|
|
86
|
+
* for its bounded output-ordering grace. It is not an active provider turn. */
|
|
87
|
+
private pendingCompletionTurnId;
|
|
72
88
|
private assistantMessageTimer;
|
|
73
89
|
private deferredAssistantMessage;
|
|
74
90
|
private pendingCompletion;
|
|
91
|
+
private readonly pendingMcpServers;
|
|
92
|
+
private readonly failedMcpServers;
|
|
93
|
+
private mcpStartupTimer;
|
|
94
|
+
private lastMcpStatusNote;
|
|
75
95
|
/** Completed-item dedup keys already mirrored (live vs resume backfill). Key =
|
|
76
96
|
* `threadId:turnId:item.id`; anonymous items use a per-(thread,turn) position
|
|
77
97
|
* counter. Mirrors reference implementation `_completed_item_key` + `synced_item_keys`. */
|
|
@@ -83,12 +103,29 @@ export declare class CodexSessionForwarder {
|
|
|
83
103
|
/** Begin mirroring. Idempotent. */
|
|
84
104
|
start(): void;
|
|
85
105
|
stop(): void;
|
|
86
|
-
/** True while
|
|
106
|
+
/** True while the provider owns an active turn, including the short interval
|
|
107
|
+
* between injection acceptance and observer confirmation. */
|
|
87
108
|
isTurnOpen(): boolean;
|
|
88
109
|
/** The current turn's codex id (only meaningful while {@link isTurnOpen}). */
|
|
89
110
|
currentTurnId(): string | null;
|
|
111
|
+
/**
|
|
112
|
+
* Record a turn accepted by the injection connection before the independent
|
|
113
|
+
* observer receives `turn/started`. This closes the read-decide-RPC-write race:
|
|
114
|
+
* another web message arriving in that window must steer this turn, not start
|
|
115
|
+
* a second one.
|
|
116
|
+
*/
|
|
117
|
+
noteTurnAccepted(turnId: string): void;
|
|
118
|
+
hasPendingMcpStartup(): boolean;
|
|
119
|
+
/** Mark and return the startup servers cancelled by a web Stop. */
|
|
120
|
+
cancelMcpStartup(): string[];
|
|
121
|
+
/** Diagnostic suffix for an injection failure during Provider startup. */
|
|
122
|
+
mcpStartupDetail(): string | null;
|
|
90
123
|
/** The bound codex thread id captured from `thread/started` (null until then). */
|
|
91
124
|
threadId(): string | null;
|
|
125
|
+
/** Seed an already-persisted/resumed thread binding. The bridge retains this
|
|
126
|
+
* state even when app-server does not rebroadcast `thread/started`, so
|
|
127
|
+
* terminal-boundary recovery must know it too. */
|
|
128
|
+
noteThreadBound(threadId: string): void;
|
|
92
129
|
/**
|
|
93
130
|
* Replay the backlog turns from a `thread/resume` response as if they were live
|
|
94
131
|
* `item/completed` notifications — the fresh-thread first-turn backfill. Each
|
|
@@ -97,11 +134,25 @@ export declare class CodexSessionForwarder {
|
|
|
97
134
|
* not doubled.
|
|
98
135
|
*/
|
|
99
136
|
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
|
+
/** Fail an open response exactly once when its observer or terminal exits. */
|
|
145
|
+
failOpenTurn(error: Error): boolean;
|
|
100
146
|
private handle;
|
|
101
147
|
private scheduleCompletion;
|
|
102
148
|
private refreshCompletionGrace;
|
|
103
149
|
private flushPendingCompletion;
|
|
104
150
|
private settle;
|
|
151
|
+
private handleMcpStartupStatus;
|
|
152
|
+
private settleMcpStartup;
|
|
153
|
+
private clearMcpStartupTimer;
|
|
154
|
+
private emitMcpStartupStatus;
|
|
155
|
+
private emitMcpStatus;
|
|
105
156
|
/** Map + emit one completed codex item, deduped by a TOTAL key and routing the
|
|
106
157
|
* user echo to {@link CodexForwarderSink.onUserMessage}. Shared by live + backfill. */
|
|
107
158
|
private processCompletedItem;
|
|
@@ -118,4 +169,17 @@ export declare class CodexSessionForwarder {
|
|
|
118
169
|
private completedItemKey;
|
|
119
170
|
private advanceAnonCounter;
|
|
120
171
|
private ensureTurn;
|
|
172
|
+
/** Start (or confirm) the app-server's authoritative active turn. A newer
|
|
173
|
+
* start supersedes an older response whose terminal edge arrived late; a
|
|
174
|
+
* pending Traex completion is flushed first so its final item grace remains
|
|
175
|
+
* intact. */
|
|
176
|
+
private beginTurn;
|
|
177
|
+
/** Active-turn clearing contract:
|
|
178
|
+
*
|
|
179
|
+
* - an identified active turn is closed only by the same id;
|
|
180
|
+
* - an id-less boundary cannot close an identified active turn;
|
|
181
|
+
* - with no observed active turn, an identified boundary may recover a
|
|
182
|
+
* missed start only when it carries the currently-bound thread id. */
|
|
183
|
+
private terminalBoundaryMatchesActiveTurn;
|
|
184
|
+
private notificationMatchesCurrentThread;
|
|
121
185
|
}
|