@rynx-ai/runtime 0.1.11-beta.22 → 0.1.11-beta.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude/executor.d.ts +19 -5
- package/dist/claude/executor.js +56 -12
- package/dist/claude/native-integration.d.ts +7 -0
- package/dist/claude/native-integration.js +25 -2
- package/dist/codex-app-server/forwarder.d.ts +2 -0
- package/dist/codex-app-server/forwarder.js +12 -2
- package/dist/codex-app-server/mapping.d.ts +1 -0
- package/dist/codex-app-server/mapping.js +8 -1
- package/dist/host.d.ts +9 -4
- package/dist/host.js +262 -95
- package/dist/index.d.ts +1 -1
- package/dist/runner/child.d.ts +6 -2
- package/dist/runner/child.js +59 -14
- package/dist/runner/manager.d.ts +6 -9
- package/dist/runner/manager.js +29 -50
- package/dist/runner/protocol.d.ts +6 -6
- package/dist/terminal/registry.js +3 -2
- package/dist/terminal/tmux.d.ts +29 -5
- package/dist/terminal/tmux.js +98 -52
- package/package.json +2 -2
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import type { SkillMeta } from "@rynx-ai/core";
|
|
2
|
+
export interface MaterializedClaudePlugin {
|
|
3
|
+
pluginDir: string;
|
|
4
|
+
}
|
|
5
|
+
/** Stable Claude `--plugin-dir` owned by one Rynx Session. */
|
|
6
|
+
export declare function claudePluginDir(sessionId: string): string;
|
|
2
7
|
/**
|
|
3
|
-
*
|
|
8
|
+
* Return the existing Session-owned plugin directory as-is. It is the durable
|
|
9
|
+
* Provider input for this Session, not an immutable shared cache: runtime files
|
|
10
|
+
* created inside it (for example Python `__pycache__`) remain part of that
|
|
11
|
+
* Session and do not trigger SkillRef replay.
|
|
12
|
+
*/
|
|
13
|
+
export declare function reuseClaudePlugin(sessionId: string): Promise<MaterializedClaudePlugin | null>;
|
|
14
|
+
/**
|
|
15
|
+
* Materialize a selected catalog skill subset into a Session-owned Claude
|
|
4
16
|
* `--plugin-dir` (a dir with `skills/<name>/SKILL.md` + a `.claude-plugin/plugin.json`
|
|
5
17
|
* manifest — claude's mechanism for exposing skills outside its host dirs, since
|
|
6
18
|
* it doesn't scan `~/.rynx/skills`). Consumed by the claude-native live launch.
|
|
7
19
|
*
|
|
20
|
+
* The committed path is deterministic and survives Provider/runner restarts.
|
|
21
|
+
* An existing directory is returned as-is. The first materialization is built
|
|
22
|
+
* in a sibling staging directory and renamed into the stable path so a crash
|
|
23
|
+
* cannot expose a partially copied plugin.
|
|
24
|
+
*
|
|
8
25
|
* Returns `null` when there's nothing to add. Rynx-managed Claude sessions load
|
|
9
26
|
* no host/project/local setting sources, so this explicit plugin directory is
|
|
10
27
|
* the only native skill channel; the selected subset is enforced exactly.
|
|
11
28
|
*/
|
|
12
|
-
export declare function
|
|
13
|
-
pluginDir?: string;
|
|
14
|
-
cleanup?: () => Promise<void>;
|
|
15
|
-
} | null>;
|
|
29
|
+
export declare function materializeClaudePlugin(sessionId: string, selected: SkillMeta[] | null | undefined): Promise<MaterializedClaudePlugin | null>;
|
package/dist/claude/executor.js
CHANGED
|
@@ -1,26 +1,70 @@
|
|
|
1
|
-
import { cp, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
1
|
+
import { cp, mkdir, mkdtemp, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import { runtimeSessionStateDir } from "../runtime-state-paths.js";
|
|
4
|
+
/** Stable Claude `--plugin-dir` owned by one Rynx Session. */
|
|
5
|
+
export function claudePluginDir(sessionId) {
|
|
6
|
+
return path.join(runtimeSessionStateDir(sessionId), "claude-plugin");
|
|
7
|
+
}
|
|
4
8
|
/**
|
|
5
|
-
*
|
|
9
|
+
* Return the existing Session-owned plugin directory as-is. It is the durable
|
|
10
|
+
* Provider input for this Session, not an immutable shared cache: runtime files
|
|
11
|
+
* created inside it (for example Python `__pycache__`) remain part of that
|
|
12
|
+
* Session and do not trigger SkillRef replay.
|
|
13
|
+
*/
|
|
14
|
+
export async function reuseClaudePlugin(sessionId) {
|
|
15
|
+
const pluginDir = claudePluginDir(sessionId);
|
|
16
|
+
try {
|
|
17
|
+
return (await stat(pluginDir)).isDirectory() ? { pluginDir } : null;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Materialize a selected catalog skill subset into a Session-owned Claude
|
|
6
25
|
* `--plugin-dir` (a dir with `skills/<name>/SKILL.md` + a `.claude-plugin/plugin.json`
|
|
7
26
|
* manifest — claude's mechanism for exposing skills outside its host dirs, since
|
|
8
27
|
* it doesn't scan `~/.rynx/skills`). Consumed by the claude-native live launch.
|
|
9
28
|
*
|
|
29
|
+
* The committed path is deterministic and survives Provider/runner restarts.
|
|
30
|
+
* An existing directory is returned as-is. The first materialization is built
|
|
31
|
+
* in a sibling staging directory and renamed into the stable path so a crash
|
|
32
|
+
* cannot expose a partially copied plugin.
|
|
33
|
+
*
|
|
10
34
|
* Returns `null` when there's nothing to add. Rynx-managed Claude sessions load
|
|
11
35
|
* no host/project/local setting sources, so this explicit plugin directory is
|
|
12
36
|
* the only native skill channel; the selected subset is enforced exactly.
|
|
13
37
|
*/
|
|
14
|
-
export async function
|
|
38
|
+
export async function materializeClaudePlugin(sessionId, selected) {
|
|
15
39
|
if (selected == null || selected.length === 0)
|
|
16
40
|
return null;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
41
|
+
const reused = await reuseClaudePlugin(sessionId);
|
|
42
|
+
if (reused)
|
|
43
|
+
return reused;
|
|
44
|
+
const pluginDir = claudePluginDir(sessionId);
|
|
45
|
+
const stateDir = runtimeSessionStateDir(sessionId);
|
|
46
|
+
await mkdir(stateDir, { recursive: true, mode: 0o700 });
|
|
47
|
+
const stagingDir = await mkdtemp(path.join(stateDir, ".claude-plugin-stage-"));
|
|
48
|
+
try {
|
|
49
|
+
const skillsRoot = path.join(stagingDir, "skills");
|
|
50
|
+
await mkdir(skillsRoot, { recursive: true });
|
|
51
|
+
for (const skill of selected) {
|
|
52
|
+
await cp(skill.dir, path.join(skillsRoot, skill.name), { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
await mkdir(path.join(stagingDir, ".claude-plugin"), { recursive: true });
|
|
55
|
+
await writeFile(path.join(stagingDir, ".claude-plugin", "plugin.json"), `${JSON.stringify({ name: "rynx-agent-skills", description: "Per-agent skill subset" }, null, 2)}\n`);
|
|
56
|
+
try {
|
|
57
|
+
await rename(stagingDir, pluginDir);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const concurrent = await reuseClaudePlugin(sessionId);
|
|
61
|
+
if (!concurrent)
|
|
62
|
+
throw error;
|
|
63
|
+
return concurrent;
|
|
64
|
+
}
|
|
65
|
+
return { pluginDir };
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
22
69
|
}
|
|
23
|
-
await mkdir(path.join(dir, ".claude-plugin"), { recursive: true });
|
|
24
|
-
await writeFile(path.join(dir, ".claude-plugin", "plugin.json"), `${JSON.stringify({ name: "rynx-agent-skills", description: "Per-agent skill subset" }, null, 2)}\n`);
|
|
25
|
-
return { pluginDir: dir, cleanup: () => rm(dir, { recursive: true, force: true }) };
|
|
26
70
|
}
|
|
@@ -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
|