@rynx-ai/runtime 0.1.11-beta.24 → 0.1.11-beta.26
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-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 +8 -3
- package/dist/claude/native-integration.js +38 -8
- package/dist/claude/transcript.js +27 -17
- package/dist/codex-app-server/client.js +11 -1
- package/dist/codex-app-server/forwarder.d.ts +25 -1
- package/dist/codex-app-server/forwarder.js +216 -55
- package/dist/codex-app-server/mapping.d.ts +0 -6
- package/dist/codex-app-server/mapping.js +70 -5
- package/dist/codex-app-server/protocol.d.ts +35 -2
- package/dist/host.d.ts +15 -7
- package/dist/host.js +517 -50
- package/dist/input-resources.d.ts +4 -0
- package/dist/input-resources.js +21 -5
- package/dist/runner/child.d.ts +13 -13
- package/dist/runner/child.js +183 -13
- package/dist/runner/manager.d.ts +12 -4
- package/dist/runner/manager.js +204 -20
- package/dist/runner/protocol.d.ts +47 -1
- package/dist/runner/protocol.js +5 -0
- package/dist/terminal/registry.js +1 -1
- package/dist/terminal/tmux.d.ts +5 -5
- package/dist/terminal/tmux.js +6 -6
- package/package.json +2 -2
|
@@ -109,6 +109,8 @@ export interface HookEvent {
|
|
|
109
109
|
/** Claude's session uuid — subagent hooks report a `subagents/…` transcript path. */
|
|
110
110
|
sessionId?: string;
|
|
111
111
|
source?: string;
|
|
112
|
+
/** Authoritative live background-shell count carried by a Stop hook. */
|
|
113
|
+
backgroundTaskCount?: number;
|
|
112
114
|
payload: Record<string, unknown>;
|
|
113
115
|
}
|
|
114
116
|
/** Tail `hooks.jsonl` from a byte offset into parsed {@link HookEvent}s. */
|
|
@@ -267,6 +267,25 @@ function readJsonFile(path) {
|
|
|
267
267
|
function asString(value) {
|
|
268
268
|
return typeof value === "string" && value ? value : undefined;
|
|
269
269
|
}
|
|
270
|
+
const TERMINAL_BACKGROUND_TASK_STATUSES = new Set([
|
|
271
|
+
"completed",
|
|
272
|
+
"failed",
|
|
273
|
+
"stopped",
|
|
274
|
+
"killed",
|
|
275
|
+
]);
|
|
276
|
+
function backgroundTaskCount(payload) {
|
|
277
|
+
if (payload.hook_event_name !== "Stop")
|
|
278
|
+
return undefined;
|
|
279
|
+
const tasks = payload.background_tasks;
|
|
280
|
+
if (!Array.isArray(tasks))
|
|
281
|
+
return 0;
|
|
282
|
+
return tasks.filter((task) => {
|
|
283
|
+
if (!task || typeof task !== "object" || Array.isArray(task))
|
|
284
|
+
return true;
|
|
285
|
+
const status = task.status;
|
|
286
|
+
return typeof status !== "string" || !TERMINAL_BACKGROUND_TASK_STATUSES.has(status);
|
|
287
|
+
}).length;
|
|
288
|
+
}
|
|
270
289
|
export function readClaudeState(bridgeDir) {
|
|
271
290
|
const raw = readJsonFile(join(bridgeDir, STATE_FILE));
|
|
272
291
|
if (!raw || typeof raw !== "object")
|
|
@@ -348,11 +367,15 @@ export function readHookEventsFrom(bridgeDir, byteOffset) {
|
|
|
348
367
|
const payload = rec.payload;
|
|
349
368
|
if (!payload || typeof payload !== "object")
|
|
350
369
|
continue;
|
|
370
|
+
const liveBackgroundTasks = backgroundTaskCount(payload);
|
|
351
371
|
events.push({
|
|
352
372
|
eventName: asString(payload.hook_event_name),
|
|
353
373
|
transcriptPath: asString(payload.transcript_path),
|
|
354
374
|
sessionId: asString(payload.session_id),
|
|
355
375
|
source: asString(payload.source),
|
|
376
|
+
...(liveBackgroundTasks === undefined
|
|
377
|
+
? {}
|
|
378
|
+
: { backgroundTaskCount: liveBackgroundTasks }),
|
|
356
379
|
payload,
|
|
357
380
|
});
|
|
358
381
|
}
|
|
@@ -176,6 +176,43 @@ function permissionRequest(id, payload, toolInput, suggestions) {
|
|
|
176
176
|
const toolName = asString(payload.tool_name) ?? "tool";
|
|
177
177
|
const command = asString(toolInput.command) ?? asString(toolInput.file_path);
|
|
178
178
|
const cwd = asString(payload.cwd);
|
|
179
|
+
if (toolName === "ExitPlanMode") {
|
|
180
|
+
const plan = asString(toolInput.plan) ?? "Plan details were not provided by Claude.";
|
|
181
|
+
return {
|
|
182
|
+
interactionId: id,
|
|
183
|
+
kind: "permission",
|
|
184
|
+
title: "Plan review",
|
|
185
|
+
fields: [{
|
|
186
|
+
id: "feedback",
|
|
187
|
+
type: "text",
|
|
188
|
+
label: "What should change about the plan?",
|
|
189
|
+
required: false,
|
|
190
|
+
multiline: true,
|
|
191
|
+
placeholder: "Revision feedback (optional)",
|
|
192
|
+
}],
|
|
193
|
+
actions: [
|
|
194
|
+
{
|
|
195
|
+
id: "allow_auto",
|
|
196
|
+
label: "Yes, and use auto mode",
|
|
197
|
+
style: "primary",
|
|
198
|
+
requiresAnswers: false,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: "allow_manual",
|
|
202
|
+
label: "Yes, manually approve edits",
|
|
203
|
+
requiresAnswers: false,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "deny_feedback",
|
|
207
|
+
label: "Reject with feedback",
|
|
208
|
+
style: "danger",
|
|
209
|
+
requiresAnswers: false,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
context: { toolName, summary: plan },
|
|
213
|
+
createdAt: Date.now(),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
179
216
|
return {
|
|
180
217
|
interactionId: id,
|
|
181
218
|
kind: "permission",
|
|
@@ -261,6 +298,31 @@ function nativeVerdict(hookKind, payload, result, suggestions) {
|
|
|
261
298
|
},
|
|
262
299
|
};
|
|
263
300
|
}
|
|
301
|
+
if (toolName === "ExitPlanMode") {
|
|
302
|
+
const feedback = answerText(resolution.answers?.feedback).trim();
|
|
303
|
+
const behavior = resolution.actionId === "allow_auto" ||
|
|
304
|
+
resolution.actionId === "allow_manual"
|
|
305
|
+
? "allow"
|
|
306
|
+
: "deny";
|
|
307
|
+
return {
|
|
308
|
+
hookSpecificOutput: {
|
|
309
|
+
hookEventName: "PermissionRequest",
|
|
310
|
+
decision: {
|
|
311
|
+
behavior,
|
|
312
|
+
...(behavior === "deny" && feedback ? { message: feedback } : {}),
|
|
313
|
+
...(behavior === "allow"
|
|
314
|
+
? {
|
|
315
|
+
updatedPermissions: [{
|
|
316
|
+
type: "setMode",
|
|
317
|
+
mode: resolution.actionId === "allow_auto" ? "auto" : "default",
|
|
318
|
+
destination: "session",
|
|
319
|
+
}],
|
|
320
|
+
}
|
|
321
|
+
: {}),
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
264
326
|
const suggestionMatch = /^allow_suggestion_(\d+)$/.exec(resolution.actionId);
|
|
265
327
|
const suggestionIndex = suggestionMatch ? Number(suggestionMatch[1]) : -1;
|
|
266
328
|
const selectedSuggestion = Number.isSafeInteger(suggestionIndex)
|
|
@@ -21,7 +21,7 @@ export interface ClaudeForwarderSink {
|
|
|
21
21
|
onInteraction(event: RuntimeInteractionEvent): void;
|
|
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
|
-
onTurnEnd(usage?: Record<string, unknown
|
|
24
|
+
onTurnEnd(usage?: Record<string, unknown>, backgroundTaskCount?: number): void;
|
|
25
25
|
/** The current turn ended because the user explicitly interrupted it. */
|
|
26
26
|
onTurnInterrupted?(usage?: Record<string, unknown>): void;
|
|
27
27
|
/** Escape was sent for the open Turn. Publish cancelled UI state immediately;
|
|
@@ -34,8 +34,9 @@ export interface ClaudeForwarderSink {
|
|
|
34
34
|
* assistant record and the two orderings race; finalizing here would split a
|
|
35
35
|
* late assistant record into its own turn. The turn is finalized by the next
|
|
36
36
|
* user prompt or the inactivity backstop, so a late record still joins it. */
|
|
37
|
-
onIdle(): void;
|
|
38
|
-
/**
|
|
37
|
+
onIdle(backgroundTaskCount?: number): void;
|
|
38
|
+
/** Claude's current Turn or native runtime failed (StopFailure/pane exit).
|
|
39
|
+
* May fire between Turns so Session-level failure can retire stale liveness. */
|
|
39
40
|
onTurnError(error: Error): void;
|
|
40
41
|
/** Fired once SessionStart reveals claude's session id + transcript path, so
|
|
41
42
|
* the host can persist the id and release its readiness gate. */
|
|
@@ -115,6 +116,10 @@ export declare class ClaudeLiveSession {
|
|
|
115
116
|
private lastActivityAt;
|
|
116
117
|
/** When the Stop hook fired (null = not yet) — drives the grace-period close. */
|
|
117
118
|
private stopPendingAt;
|
|
119
|
+
/** Authoritative live background-shell count from the pending Stop hook. */
|
|
120
|
+
private stopBackgroundTaskCount;
|
|
121
|
+
/** Whether the pending Stop count already rode an immediate idle edge. */
|
|
122
|
+
private stopBackgroundTaskCountDelivered;
|
|
118
123
|
/** An idle status from Claude's session metadata. The short delay keeps a
|
|
119
124
|
* final transcript record in the response without letting stale tool state
|
|
120
125
|
* override the provider's terminal status. */
|
|
@@ -29,7 +29,7 @@ const MAX_SUBMISSION_OBSERVATIONS = 64;
|
|
|
29
29
|
const INTERACTION_ACK_TIMEOUT_MS = 5_000;
|
|
30
30
|
// Claude writes this synthetic user record after Escape (including a tool-use
|
|
31
31
|
// interruption). It is lifecycle, not a new prompt. Kept aligned with
|
|
32
|
-
//
|
|
32
|
+
// reference implementation's `_CLAUDE_INTERRUPT_RECORD_RE`.
|
|
33
33
|
const CLAUDE_INTERRUPT_RECORD_RE = /^\[Request interrupted by user(?: for tool use)?\]$/;
|
|
34
34
|
const INTERACTION_LEASE_TIMEOUT_MS = 30_000;
|
|
35
35
|
function processIsAlive(pid) {
|
|
@@ -177,6 +177,10 @@ export class ClaudeLiveSession {
|
|
|
177
177
|
lastActivityAt = 0;
|
|
178
178
|
/** When the Stop hook fired (null = not yet) — drives the grace-period close. */
|
|
179
179
|
stopPendingAt = null;
|
|
180
|
+
/** Authoritative live background-shell count from the pending Stop hook. */
|
|
181
|
+
stopBackgroundTaskCount;
|
|
182
|
+
/** Whether the pending Stop count already rode an immediate idle edge. */
|
|
183
|
+
stopBackgroundTaskCountDelivered = false;
|
|
180
184
|
/** An idle status from Claude's session metadata. The short delay keeps a
|
|
181
185
|
* final transcript record in the response without letting stale tool state
|
|
182
186
|
* override the provider's terminal status. */
|
|
@@ -436,11 +440,15 @@ export class ClaudeLiveSession {
|
|
|
436
440
|
if (ev.eventName === "StopFailure") {
|
|
437
441
|
this.stopSignalPending = false;
|
|
438
442
|
this.stopPendingAt = null;
|
|
443
|
+
this.stopBackgroundTaskCount = undefined;
|
|
444
|
+
this.stopBackgroundTaskCountDelivered = false;
|
|
439
445
|
this.turnFailurePending = new Error("claude turn failed");
|
|
440
446
|
}
|
|
441
447
|
else {
|
|
442
448
|
this.stopSignalPending = true;
|
|
443
449
|
this.stopPendingAt = this.now(); // close after a short grace (late assistant flush)
|
|
450
|
+
this.stopBackgroundTaskCount = ev.backgroundTaskCount ?? 0;
|
|
451
|
+
this.stopBackgroundTaskCountDelivered = false;
|
|
444
452
|
}
|
|
445
453
|
}
|
|
446
454
|
}
|
|
@@ -452,7 +460,10 @@ export class ClaudeLiveSession {
|
|
|
452
460
|
return;
|
|
453
461
|
this.turnFailurePending = null;
|
|
454
462
|
this.providerFailureSticky = true;
|
|
455
|
-
this.
|
|
463
|
+
if (this.turnOpen)
|
|
464
|
+
this.closeTurnError(error);
|
|
465
|
+
else
|
|
466
|
+
this.sink.onTurnError(error);
|
|
456
467
|
}
|
|
457
468
|
/** Emit the Stop idle signal only after this tick has discovered native
|
|
458
469
|
* interactions. A pending question/permission is active execution, not idle. */
|
|
@@ -460,10 +471,10 @@ export class ClaudeLiveSession {
|
|
|
460
471
|
if (!this.stopSignalPending)
|
|
461
472
|
return;
|
|
462
473
|
this.stopSignalPending = false;
|
|
463
|
-
if (this.
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
474
|
+
if (this.pendingInteractions.size === 0) {
|
|
475
|
+
this.sink.onIdle(this.stopBackgroundTaskCount);
|
|
476
|
+
this.stopBackgroundTaskCountDelivered = true;
|
|
477
|
+
}
|
|
467
478
|
}
|
|
468
479
|
/** SessionStart drives discovery (first) and rotation (a later one with a NEW
|
|
469
480
|
* session id + transcript). `/clear` → source="clear"; `/fork` → source="resume"
|
|
@@ -907,6 +918,8 @@ export class ClaudeLiveSession {
|
|
|
907
918
|
this.providerIdleAt = null;
|
|
908
919
|
this.openToolIds.clear();
|
|
909
920
|
this.stopPendingAt = null;
|
|
921
|
+
this.stopBackgroundTaskCount = undefined;
|
|
922
|
+
this.stopBackgroundTaskCountDelivered = false;
|
|
910
923
|
this.sink.onTurnStart(this.currentTurnId);
|
|
911
924
|
}
|
|
912
925
|
this.sink.onUserMessage(content);
|
|
@@ -1040,6 +1053,11 @@ export class ClaudeLiveSession {
|
|
|
1040
1053
|
this.stopPendingAt = null;
|
|
1041
1054
|
if (status.runnerStatus === "running") {
|
|
1042
1055
|
this.providerFailureSticky = false;
|
|
1056
|
+
// A new native busy interval supersedes the prior Stop snapshot. The
|
|
1057
|
+
// Session running edge clears its sticky tally; do not re-emit that old
|
|
1058
|
+
// count if the previous transcript Turn closes a moment later.
|
|
1059
|
+
this.stopBackgroundTaskCount = undefined;
|
|
1060
|
+
this.stopBackgroundTaskCountDelivered = false;
|
|
1043
1061
|
}
|
|
1044
1062
|
else if (this.providerFailureSticky) {
|
|
1045
1063
|
return;
|
|
@@ -1114,6 +1132,8 @@ export class ClaudeLiveSession {
|
|
|
1114
1132
|
this.providerIdleAt = null;
|
|
1115
1133
|
this.turnOpen = true;
|
|
1116
1134
|
this.turnInterrupted = false;
|
|
1135
|
+
this.stopBackgroundTaskCount = undefined;
|
|
1136
|
+
this.stopBackgroundTaskCountDelivered = false;
|
|
1117
1137
|
this.sink.onTurnStart(this.currentTurnId);
|
|
1118
1138
|
}
|
|
1119
1139
|
/** Mirror a local `!` command as its own mini-turn: close any open turn, then
|
|
@@ -1162,7 +1182,10 @@ export class ClaudeLiveSession {
|
|
|
1162
1182
|
/** Retire process-scoped metadata before classifying a terminal exit. */
|
|
1163
1183
|
noteTerminalExit(error) {
|
|
1164
1184
|
this.statusPoller?.retire();
|
|
1165
|
-
|
|
1185
|
+
const failedOpenTurn = this.failOpenTurn(error);
|
|
1186
|
+
if (!failedOpenTurn)
|
|
1187
|
+
this.sink.onTurnError(error);
|
|
1188
|
+
return failedOpenTurn;
|
|
1166
1189
|
}
|
|
1167
1190
|
closeTurn() {
|
|
1168
1191
|
if (!this.turnOpen)
|
|
@@ -1179,10 +1202,15 @@ export class ClaudeLiveSession {
|
|
|
1179
1202
|
const interrupted = this.turnInterrupted;
|
|
1180
1203
|
this.turnInterrupted = false;
|
|
1181
1204
|
const usage = this.statusUsage();
|
|
1205
|
+
const backgroundTaskCount = this.stopBackgroundTaskCountDelivered
|
|
1206
|
+
? undefined
|
|
1207
|
+
: this.stopBackgroundTaskCount;
|
|
1208
|
+
this.stopBackgroundTaskCount = undefined;
|
|
1209
|
+
this.stopBackgroundTaskCountDelivered = false;
|
|
1182
1210
|
if (interrupted && this.sink.onTurnInterrupted)
|
|
1183
1211
|
this.sink.onTurnInterrupted(usage);
|
|
1184
1212
|
else
|
|
1185
|
-
this.sink.onTurnEnd(usage);
|
|
1213
|
+
this.sink.onTurnEnd(usage, backgroundTaskCount);
|
|
1186
1214
|
}
|
|
1187
1215
|
closeTurnError(error) {
|
|
1188
1216
|
if (!this.turnOpen)
|
|
@@ -1196,6 +1224,8 @@ export class ClaudeLiveSession {
|
|
|
1196
1224
|
this.turnInterrupted = false;
|
|
1197
1225
|
this.stopSignalPending = false;
|
|
1198
1226
|
this.stopPendingAt = null;
|
|
1227
|
+
this.stopBackgroundTaskCount = undefined;
|
|
1228
|
+
this.stopBackgroundTaskCountDelivered = false;
|
|
1199
1229
|
this.resetMessageCorrelation();
|
|
1200
1230
|
this.sink.onTurnError(error);
|
|
1201
1231
|
}
|
|
@@ -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
|
});
|
|
@@ -1386,7 +1386,17 @@ export function buildTextUserInput(message) {
|
|
|
1386
1386
|
export function buildRuntimeUserInput(input) {
|
|
1387
1387
|
return input.content.map((part) => part.type === "text"
|
|
1388
1388
|
? { type: "text", text: part.text, text_elements: [] }
|
|
1389
|
-
:
|
|
1389
|
+
: part.type === "local_image"
|
|
1390
|
+
? { type: "localImage", path: part.path }
|
|
1391
|
+
: {
|
|
1392
|
+
type: "text",
|
|
1393
|
+
text: `[[RYNX_FILE_RESOURCE ${JSON.stringify({
|
|
1394
|
+
path: part.path,
|
|
1395
|
+
...(part.resource.filename ? { filename: part.resource.filename } : {}),
|
|
1396
|
+
mediaType: part.resource.mediaType,
|
|
1397
|
+
})}]]\nInspect this absolute file path with the available file-reading tools before answering.`,
|
|
1398
|
+
text_elements: [],
|
|
1399
|
+
});
|
|
1390
1400
|
}
|
|
1391
1401
|
const defaultLogger = {
|
|
1392
1402
|
log(entry) {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import type { AgentEvent, UserContentPart } from "@rynx-ai/core";
|
|
25
25
|
import type { CodexAppServerClient } from "./client.js";
|
|
26
26
|
import type { McpStartupPlan } from "./mcp-startup.js";
|
|
27
|
-
import type { ResumedTurn } from "./protocol.js";
|
|
27
|
+
import type { CollaborationModeKind, ResumedTurn } from "./protocol.js";
|
|
28
28
|
export interface CodexForwarderSink {
|
|
29
29
|
/** A turn began. `turnId` is codex's turn id, used to derive a stable
|
|
30
30
|
* `responseId`. Start a fresh normalizer/response. */
|
|
@@ -34,6 +34,9 @@ export interface CodexForwarderSink {
|
|
|
34
34
|
onTurnObserved?(turnId?: string): void;
|
|
35
35
|
/** One mapped event within the current turn. */
|
|
36
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;
|
|
37
40
|
/** Provider startup is session status, not a model item. Hosts that already
|
|
38
41
|
* published the response can forward it without synthesizing another start. */
|
|
39
42
|
onStatus?(note: string | undefined, statusKind?: "startup"): void;
|
|
@@ -49,6 +52,8 @@ export interface CodexForwarderSink {
|
|
|
49
52
|
/** The user's turn text (sourced from codex's `userMessage` item), so a
|
|
50
53
|
* co-driving TUI's prompt is recorded even though this process never injected it. */
|
|
51
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;
|
|
52
57
|
/** A managed Core fork also broadcasts `thread/started`, but does not switch
|
|
53
58
|
* the source TUI. Discard that notification before changing the bound thread. */
|
|
54
59
|
shouldIgnoreThreadStarted?(threadId: string, forkedFromId?: string): boolean;
|
|
@@ -58,6 +63,16 @@ export interface CodexForwarderSink {
|
|
|
58
63
|
/** The thread showed activity (a turn/item began, so its rollout now exists).
|
|
59
64
|
* Fired once; lets a parked `thread/resume` retry (reference implementation's ready signal). */
|
|
60
65
|
onThreadActive?(): void;
|
|
66
|
+
/** The native TUI or another app-server client changed collaboration mode. */
|
|
67
|
+
onCollaborationModeChanged?(mode: CollaborationModeKind): void;
|
|
68
|
+
/** Codex's terminal-local Plan picker is not emitted by app-server today.
|
|
69
|
+
* Synthesize it only after a live Plan item and its Turn both complete. */
|
|
70
|
+
onPlanImplementationPrompt?(prompt: CodexPlanImplementationPrompt): void;
|
|
71
|
+
}
|
|
72
|
+
export interface CodexPlanImplementationPrompt {
|
|
73
|
+
threadId: string;
|
|
74
|
+
turnId: string;
|
|
75
|
+
text: string;
|
|
61
76
|
}
|
|
62
77
|
export interface CodexSessionForwarderOptions {
|
|
63
78
|
/** Some Codex-lineage runtimes publish the final item one frame after
|
|
@@ -101,6 +116,11 @@ export declare class CodexSessionForwarder {
|
|
|
101
116
|
/** Per-(thread,turn) position counter for items lacking a stable codex id
|
|
102
117
|
* (peek-then-advance; advanced only on a successful claim). reference implementation anon path. */
|
|
103
118
|
private readonly anonCounters;
|
|
119
|
+
private pendingPlanImplementation;
|
|
120
|
+
/** Latest aggregate diff per Turn. Codex republishes the complete diff after
|
|
121
|
+
* every edit; only the terminal snapshot belongs in transcript history. */
|
|
122
|
+
private readonly turnDiffByTurn;
|
|
123
|
+
private replayingBackfill;
|
|
104
124
|
constructor(client: CodexAppServerClient, sink: CodexForwarderSink, options?: CodexSessionForwarderOptions);
|
|
105
125
|
/** Begin mirroring. Idempotent. */
|
|
106
126
|
start(): void;
|
|
@@ -136,6 +156,9 @@ export declare class CodexSessionForwarder {
|
|
|
136
156
|
* not doubled.
|
|
137
157
|
*/
|
|
138
158
|
replayBackfill(turns: ResumedTurn[]): void;
|
|
159
|
+
/** Suppress our synthesized picker when a future app-server emits the native
|
|
160
|
+
* `plan_implementation` request itself. */
|
|
161
|
+
noteNativePlanImplementationPrompt(turnId?: string): void;
|
|
139
162
|
/** Fail an open response exactly once when its observer or terminal exits. */
|
|
140
163
|
failOpenTurn(error: Error): boolean;
|
|
141
164
|
private handle;
|
|
@@ -164,6 +187,7 @@ export declare class CodexSessionForwarder {
|
|
|
164
187
|
private completedItemKey;
|
|
165
188
|
private advanceAnonCounter;
|
|
166
189
|
private ensureTurn;
|
|
190
|
+
private consumeTurnDiff;
|
|
167
191
|
/** Start (or confirm) the app-server's authoritative active turn. A newer
|
|
168
192
|
* start supersedes an older response whose terminal edge arrived late; a
|
|
169
193
|
* pending Traex completion is flushed first so its final item grace remains
|