@rynx-ai/runtime 0.1.11-beta.4 → 0.1.11-beta.41

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.
Files changed (65) hide show
  1. package/dist/claude/executor.d.ts +19 -5
  2. package/dist/claude/executor.js +56 -12
  3. package/dist/claude/models.d.ts +0 -5
  4. package/dist/claude/models.js +1 -7
  5. package/dist/claude/native-bridge.d.ts +103 -1
  6. package/dist/claude/native-bridge.js +445 -30
  7. package/dist/claude/native-hook-main.js +81 -1
  8. package/dist/claude/native-hooks.js +7 -0
  9. package/dist/claude/native-integration.d.ts +178 -26
  10. package/dist/claude/native-integration.js +1528 -170
  11. package/dist/claude/session-status.d.ts +39 -0
  12. package/dist/claude/session-status.js +163 -0
  13. package/dist/claude/transcript-clone.d.ts +18 -0
  14. package/dist/claude/transcript-clone.js +497 -0
  15. package/dist/claude/transcript.d.ts +27 -4
  16. package/dist/claude/transcript.js +158 -47
  17. package/dist/codex-app-server/client.d.ts +10 -6
  18. package/dist/codex-app-server/client.js +67 -15
  19. package/dist/codex-app-server/forwarder.d.ts +92 -3
  20. package/dist/codex-app-server/forwarder.js +532 -57
  21. package/dist/codex-app-server/mapping.d.ts +3 -6
  22. package/dist/codex-app-server/mapping.js +206 -36
  23. package/dist/codex-app-server/mcp-startup.d.ts +13 -0
  24. package/dist/codex-app-server/mcp-startup.js +63 -0
  25. package/dist/codex-app-server/process-registry.d.ts +36 -0
  26. package/dist/codex-app-server/process-registry.js +320 -0
  27. package/dist/codex-app-server/protocol.d.ts +64 -7
  28. package/dist/codex-app-server/ws-channel.d.ts +7 -0
  29. package/dist/codex-app-server/ws-channel.js +104 -28
  30. package/dist/codex-home.d.ts +35 -3
  31. package/dist/codex-home.js +323 -18
  32. package/dist/codex-session-store.d.ts +23 -0
  33. package/dist/codex-session-store.js +21 -0
  34. package/dist/host.d.ts +103 -46
  35. package/dist/host.js +1988 -634
  36. package/dist/index.d.ts +3 -3
  37. package/dist/index.js +1 -1
  38. package/dist/input-resources.d.ts +4 -0
  39. package/dist/input-resources.js +21 -5
  40. package/dist/models-catalog.d.ts +2 -1
  41. package/dist/models-catalog.js +94 -6
  42. package/dist/runner/child.d.ts +97 -28
  43. package/dist/runner/child.js +1486 -100
  44. package/dist/runner/manager.d.ts +110 -29
  45. package/dist/runner/manager.js +1481 -246
  46. package/dist/runner/protocol.d.ts +212 -24
  47. package/dist/runner/protocol.js +5 -0
  48. package/dist/runner/startup-policy.d.ts +7 -0
  49. package/dist/runner/startup-policy.js +10 -0
  50. package/dist/runner/transport.d.ts +18 -2
  51. package/dist/runner/transport.js +82 -3
  52. package/dist/runner-main.js +8 -3
  53. package/dist/terminal/claude-tui.d.ts +3 -1
  54. package/dist/terminal/claude-tui.js +3 -1
  55. package/dist/terminal/codex-tui.d.ts +4 -0
  56. package/dist/terminal/codex-tui.js +5 -0
  57. package/dist/terminal/control-parser.d.ts +39 -0
  58. package/dist/terminal/control-parser.js +172 -0
  59. package/dist/terminal/registry.d.ts +18 -15
  60. package/dist/terminal/registry.js +44 -23
  61. package/dist/terminal/spool.d.ts +47 -0
  62. package/dist/terminal/spool.js +231 -0
  63. package/dist/terminal/tmux.d.ts +126 -74
  64. package/dist/terminal/tmux.js +807 -211
  65. package/package.json +4 -4
@@ -3,7 +3,8 @@
3
3
  * interaction hooks rendezvous with the runner through the session bridge. */
4
4
  import { createHash, randomUUID } from "node:crypto";
5
5
  import { realpathSync } from "node:fs";
6
- import { claimInteractionResult, recordInteractionAck, recordHookEvent, recordInteractionRequest, removeClaimedInteractionResult, removeInteractionLease, touchInteractionLease, } from "./native-bridge.js";
6
+ import { claimInteractionResult, annotateClaudeResumeContext, recordInteractionAck, recordHookEvent, recordInteractionRequest, removeClaimedInteractionResult, removeInteractionLease, touchInteractionLease, } from "./native-bridge.js";
7
+ import { waitForTranscriptForkSignal, } from "./transcript.js";
7
8
  import { boundInteractionRequest, redactInteractionResolution, } from "../interactions.js";
8
9
  function argValue(argv, flag) {
9
10
  const i = argv.indexOf(flag);
@@ -23,6 +24,21 @@ function asRecord(value) {
23
24
  function asString(value) {
24
25
  return typeof value === "string" && value ? value : undefined;
25
26
  }
27
+ async function annotateClaudeForkSignal(bridgeDir, payload) {
28
+ annotateClaudeResumeContext(bridgeDir, payload);
29
+ if (payload.hook_event_name !== "SessionStart" ||
30
+ payload.source !== "resume" ||
31
+ payload.rynx_claude_session_was_seen === true)
32
+ return;
33
+ const transcriptPath = asString(payload.transcript_path);
34
+ const sessionId = asString(payload.session_id);
35
+ const sourceSessionId = asString(payload.rynx_previous_claude_session_id);
36
+ if (!transcriptPath || !sessionId || !sourceSessionId)
37
+ return;
38
+ const recordedAt = Date.now();
39
+ if (await waitForTranscriptForkSignal(transcriptPath, sessionId, sourceSessionId, recordedAt))
40
+ payload.rynx_fork_detected = true;
41
+ }
26
42
  function interactionId(payload) {
27
43
  // PermissionRequest does not carry a tool_use_id. A per-process nonce keeps
28
44
  // repeated or concurrent identical prompts from collapsing into one bridge id.
@@ -176,6 +192,43 @@ function permissionRequest(id, payload, toolInput, suggestions) {
176
192
  const toolName = asString(payload.tool_name) ?? "tool";
177
193
  const command = asString(toolInput.command) ?? asString(toolInput.file_path);
178
194
  const cwd = asString(payload.cwd);
195
+ if (toolName === "ExitPlanMode") {
196
+ const plan = asString(toolInput.plan) ?? "Plan details were not provided by Claude.";
197
+ return {
198
+ interactionId: id,
199
+ kind: "permission",
200
+ title: "Plan review",
201
+ fields: [{
202
+ id: "feedback",
203
+ type: "text",
204
+ label: "What should change about the plan?",
205
+ required: false,
206
+ multiline: true,
207
+ placeholder: "Revision feedback (optional)",
208
+ }],
209
+ actions: [
210
+ {
211
+ id: "allow_auto",
212
+ label: "Yes, and use auto mode",
213
+ style: "primary",
214
+ requiresAnswers: false,
215
+ },
216
+ {
217
+ id: "allow_manual",
218
+ label: "Yes, manually approve edits",
219
+ requiresAnswers: false,
220
+ },
221
+ {
222
+ id: "deny_feedback",
223
+ label: "Reject with feedback",
224
+ style: "danger",
225
+ requiresAnswers: false,
226
+ },
227
+ ],
228
+ context: { toolName, summary: plan },
229
+ createdAt: Date.now(),
230
+ };
231
+ }
179
232
  return {
180
233
  interactionId: id,
181
234
  kind: "permission",
@@ -261,6 +314,32 @@ function nativeVerdict(hookKind, payload, result, suggestions) {
261
314
  },
262
315
  };
263
316
  }
317
+ if (toolName === "ExitPlanMode") {
318
+ const feedback = answerText(resolution.answers?.feedback).trim();
319
+ const behavior = resolution.actionId === "allow_auto" ||
320
+ resolution.actionId === "allow_manual"
321
+ ? "allow"
322
+ : "deny";
323
+ return {
324
+ hookSpecificOutput: {
325
+ hookEventName: "PermissionRequest",
326
+ decision: {
327
+ behavior,
328
+ ...(behavior === "deny" && feedback ? { message: feedback } : {}),
329
+ ...(behavior === "allow"
330
+ ? {
331
+ updatedInput: toolInput,
332
+ updatedPermissions: [{
333
+ type: "setMode",
334
+ mode: resolution.actionId === "allow_auto" ? "auto" : "default",
335
+ destination: "session",
336
+ }],
337
+ }
338
+ : {}),
339
+ },
340
+ },
341
+ };
342
+ }
264
343
  const suggestionMatch = /^allow_suggestion_(\d+)$/.exec(resolution.actionId);
265
344
  const suggestionIndex = suggestionMatch ? Number(suggestionMatch[1]) : -1;
266
345
  const selectedSuggestion = Number.isSafeInteger(suggestionIndex)
@@ -301,6 +380,7 @@ async function main() {
301
380
  }
302
381
  if (!hookKind) {
303
382
  try {
383
+ await annotateClaudeForkSignal(bridgeDir, payload);
304
384
  recordHookEvent(bridgeDir, payload);
305
385
  }
306
386
  catch {
@@ -48,6 +48,13 @@ export function buildClaudeHookSettings(options) {
48
48
  SessionStart: [{ hooks: [observerHook] }],
49
49
  Stop: [{ hooks: [observerHook] }],
50
50
  StopFailure: [{ hooks: [observerHook] }],
51
+ TaskCreated: [{ hooks: [observerHook] }],
52
+ TaskCompleted: [{ hooks: [observerHook] }],
53
+ PostToolUse: [
54
+ { matcher: "TodoWrite", hooks: [observerHook] },
55
+ { matcher: "TaskUpdate", hooks: [observerHook] },
56
+ ],
57
+ PreCompact: [{ hooks: [observerHook] }],
51
58
  };
52
59
  if (options.permissionMode === "bypassPermissions") {
53
60
  const ask = shJoin([node, entry, "ask-user-question", "--bridge-dir", options.bridgeDir]);
@@ -1,18 +1,49 @@
1
1
  import type { AgentEvent, SessionInteractionResolution, TerminalCommandData, TodoItem } from "@rynx-ai/core";
2
2
  import type { ResolveInteractionResult, RuntimeInteractionEvent } from "../interactions.js";
3
+ import { type ClaudeRunnerStatus } from "./session-status.js";
3
4
  /** Mirror sink — the same shape as `CodexForwarderSink` so the host reuses one
4
5
  * per-turn normalizer wiring for both runtimes. */
5
6
  export interface ClaudeForwarderSink {
7
+ /** Begin one transcript source record. The host derives per-item source keys
8
+ * from this stable base while still running the normalizer for already-seen
9
+ * items so later records recover the same turn/tool state. */
10
+ onTranscriptRecordStart?(sourceBase: string, isHandled: (sourceId: string) => boolean, delivery?: {
11
+ lane?: string;
12
+ deadLetterPath?: string;
13
+ /** Persist an individual handled item before the containing record's
14
+ * byte cursor is allowed to advance. */
15
+ onHandled?: (sourceId: string) => void;
16
+ }): void;
17
+ /** Resolve only after every newly emitted durable item from the current
18
+ * record has reached a handled delivery outcome. Returns all per-item keys. */
19
+ onTranscriptRecordEnd?(): Promise<readonly string[]>;
6
20
  /** A turn began; `turnId` (the user record uuid) derives a stable responseId. */
7
21
  onTurnStart(turnId?: string): void;
8
22
  /** The user's prompt text (a `role:user` conversation message — not identity). */
9
23
  onUserMessage(text: string): void;
24
+ /** Provider-generated task notification persisted as hidden model context.
25
+ * It owns an independent response identity and never changes the parent Turn. */
26
+ onMetaUserMessage?(text: string, sourceKey?: string, parentToolCallId?: string): void;
27
+ /** Claude Code native sub-agent task prompt, nested under the parent Task
28
+ * tool call instead of opening a parent Session turn. */
29
+ onSubagentUserMessage?(text: string, parentToolCallId: string): void;
30
+ /** A native sub-agent's top-level `!cmd`, nested under its parent Task. */
31
+ onSubagentTerminalCommand?(cmd: TerminalCommandData, parentToolCallId: string): void;
32
+ /** Resolve/publish the canonical parent Turn for a native sub-agent. This
33
+ * mapping is persisted beside the independent child cursor. */
34
+ resolveSubagentParentResponseId?(parentToolCallId: string): string | undefined;
35
+ onSubagentParentResponse?(parentToolCallId: string, responseId: string): void;
10
36
  /** A local shell command the user ran in the TUI (claude `!` bash mode), framed
11
37
  * as its own mini-turn (an `onTurnStart`/`onTurnEnd` pair brackets this call). */
12
38
  onTerminalCommand(cmd: TerminalCommandData): void;
13
39
  /** The agent's task list changed (claude `TaskCreate`/`TaskUpdate`) — the WHOLE
14
40
  * current list (a session-level snapshot, not tied to a turn). */
15
41
  onTodos(todos: TodoItem[]): void;
42
+ /** Best-effort spinner edges around Claude's native compaction. */
43
+ onCompactionStatus?(status: "in_progress" | "completed" | "failed"): void;
44
+ /** Persist one compaction boundary. `true` means confirmed/ambiguous-handled;
45
+ * `false` means a bounded secondary hook delivery was dropped. */
46
+ onCompactionBoundary?(summary: string, sequence: number, source: "transcript" | "hook"): boolean | Promise<boolean>;
16
47
  /** One mapped event within the current turn. */
17
48
  onEvent(event: AgentEvent): void;
18
49
  /** A provider-neutral question/permission requested or settled while this
@@ -20,15 +51,23 @@ export interface ClaudeForwarderSink {
20
51
  onInteraction(event: RuntimeInteractionEvent): void;
21
52
  /** The current turn finished (a new user prompt, or the inactivity backstop).
22
53
  * `usage` carries the latest statusLine context/cost snapshot, when captured. */
23
- onTurnEnd(usage?: Record<string, unknown>): void;
24
- /** The runtime went idle (Stop hook) surface idle status WITHOUT finalizing
54
+ onTurnEnd(usage?: Record<string, unknown>, backgroundTaskCount?: number, reason?: "rotation"): void;
55
+ /** The current turn ended because the user explicitly interrupted it. */
56
+ onTurnInterrupted?(usage?: Record<string, unknown>): void;
57
+ /** Escape was sent for the open Turn. Publish cancelled UI state immediately;
58
+ * the final close remains delayed so a late transcript record can join it. */
59
+ onTurnInterruptRequested?(): void;
60
+ /** The runtime status changed according to Claude's session metadata. */
61
+ onStatus?(status: ClaudeRunnerStatus, blockedOn?: string): void;
62
+ /** The runtime went idle on the legacy hook fallback — surface idle WITHOUT finalizing
25
63
  * the turn. claude fires Stop around the same time it flushes the final
26
64
  * assistant record and the two orderings race; finalizing here would split a
27
65
  * late assistant record into its own turn. The turn is finalized by the next
28
66
  * user prompt or the inactivity backstop, so a late record still joins it. */
29
- onIdle(): void;
30
- /** The current turn failed on the runtime (StopFailure hook). */
31
- onTurnError(error: Error): void;
67
+ onIdle(backgroundTaskCount?: number, sourceId?: string): void | Promise<void>;
68
+ /** Claude's current Turn or native runtime failed (StopFailure/pane exit).
69
+ * May fire between Turns so Session-level failure can retire stale liveness. */
70
+ onTurnError(error: Error, sourceId?: string): void | Promise<void>;
32
71
  /** Fired once SessionStart reveals claude's session id + transcript path, so
33
72
  * the host can persist the id and release its readiness gate. */
34
73
  onSessionDiscovered?(claudeSessionId: string, transcriptPath: string): void;
@@ -36,10 +75,10 @@ export interface ClaudeForwarderSink {
36
75
  * binding. The host keeps the original binding and fails readiness. */
37
76
  onSessionResumeError?(error: Error): void;
38
77
  /** The claude session rotated (`/clear` → fresh, `/fork` → derived): a new
39
- * SessionStart reported a new session id + transcript. The forwarder has
40
- * already re-pointed to the new transcript and reset its per-session state; the
41
- * host mints a fresh rynx session and re-targets the mirror to it. */
42
- onSessionRotated?(kind: "clear" | "fork", claudeSessionId: string, transcriptPath: string): void;
78
+ * SessionStart reported a new session id + transcript. The old Response is
79
+ * closed first; the forwarder re-points only after this publication barrier
80
+ * resolves, so the host can mint a fresh rynx session and ACK mirror retarget. */
81
+ onSessionRotated?(kind: "clear" | "fork", claudeSessionId: string, transcriptPath: string, initialTranscriptOffset: number): void | Promise<void>;
43
82
  }
44
83
  export interface ClaudeLiveSessionOptions {
45
84
  /** The session's bridge dir (`hooks.jsonl` lives here). */
@@ -55,6 +94,10 @@ export interface ClaudeLiveSessionOptions {
55
94
  * matching forwarder cursor exists, follow only records appended after the
56
95
  * SessionStart discovery instead of mirroring native history a second time. */
57
96
  resumeAtEndOnDiscovery?: boolean;
97
+ /** Exact native-rotation boundary persisted by the host. It takes
98
+ * precedence over resumeAtEndOnDiscovery so a delayed restart cannot skip
99
+ * target records written after SessionStart. */
100
+ initialTranscriptOffsetOnDiscovery?: number;
58
101
  /** Poll interval (ms). The files are append-only, so polling is simplest. */
59
102
  pollMs?: number;
60
103
  /** Inactivity (ms) FALLBACK close for a turn whose Stop hook never fired. */
@@ -79,10 +122,28 @@ export declare class ClaudeLiveSession {
79
122
  private readonly leaseUpdatedAt;
80
123
  private started;
81
124
  private stopped;
125
+ private supervisorTask?;
126
+ private releaseSleep?;
82
127
  private hooksOffset;
128
+ private hookEventCursor;
129
+ /** Rotation publication is a checkpoint barrier. No source is polled under
130
+ * either logical Session until the daemon confirms the new binding. */
131
+ private rotationPending;
132
+ /** A non-rotation async hook (currently compaction completion) holds only the
133
+ * hook cursor and parent transcript. Native sub-agents, deltas, interactions,
134
+ * and live status continue to converge while its delivery retries. */
135
+ private compactionHookPending;
136
+ private pendingCompactionHook?;
83
137
  private interactionsOffset;
84
138
  private interactionAcksOffset;
85
139
  private transcriptOffset;
140
+ private committedTranscriptOffset;
141
+ private transcriptLineCursor;
142
+ private committedTranscriptLineCursor;
143
+ private transcriptDeliveryPending;
144
+ /** A transcript isCompactSummary boundary is currently in its durable lane.
145
+ * The completion-hook fallback must wait so the real summary remains primary. */
146
+ private transcriptCompactionPending;
86
147
  /** Secondary dedup: source ids (record uuids) already forwarded, so a re-read
87
148
  * (fingerprint reset / mid-poll death) doesn't re-emit. Persisted in the
88
149
  * forwarder state; bounded there. */
@@ -94,17 +155,40 @@ export declare class ClaudeLiveSession {
94
155
  /** Persisted native id requested through `claude --resume`. */
95
156
  private readonly expectedClaudeSessionId?;
96
157
  private readonly resumeAtEndOnDiscovery;
158
+ private readonly initialTranscriptOffsetOnDiscovery;
97
159
  /** Every claude session uuid seen — a resume into an UNSEEN id + a forkedFrom
98
160
  * marker signals a `/fork` (vs. resuming a known branch). */
99
161
  private readonly seenClaudeSessionIds;
100
162
  private turnOpen;
101
163
  private currentTurnId?;
164
+ /** Input half of a split Claude `!cmd`, plus the turn id its later output
165
+ * must reuse. Kept across a terminal Stop edge like the current terminal
166
+ * response id. */
167
+ private activeTerminalCommand?;
168
+ /** Split-command state at the latest individually handled source boundary.
169
+ * Parsing may run ahead through later records, so durable state must not use
170
+ * the mutable live value until those later items are ACKed too. */
171
+ private committedTerminalCommand?;
172
+ /** The open turn received an explicit Escape/Stop and must close cancelled. */
173
+ private turnInterrupted;
102
174
  /** The Turn opened from a pre-transcript interaction. Its unique provisional
103
175
  * id keeps responses distinct until the real prompt uuid can be adopted. */
104
176
  private syntheticTurn;
105
177
  private lastActivityAt;
106
178
  /** When the Stop hook fired (null = not yet) — drives the grace-period close. */
107
179
  private stopPendingAt;
180
+ /** Authoritative live background-shell count from the pending Stop hook. */
181
+ private stopBackgroundTaskCount;
182
+ /** Whether the pending Stop count already rode an immediate idle edge. */
183
+ private stopBackgroundTaskCountDelivered;
184
+ /** An idle status from Claude's session metadata. The short delay keeps a
185
+ * final transcript record in the response without letting stale tool state
186
+ * override the provider's terminal status. */
187
+ private providerIdleAt;
188
+ private statusPoller?;
189
+ /** A failed hook is authoritative until the next running edge. Claude writes
190
+ * idle after failures too, so that trailing file update must not erase it. */
191
+ private providerFailureSticky;
108
192
  /** Open tool-call ids (start seen, end not yet) — suppresses the idle backstop. */
109
193
  private readonly openToolIds;
110
194
  /** Transcript-backed acknowledgements for web→TUI input. A direct idle
@@ -133,8 +217,17 @@ export declare class ClaudeLiveSession {
133
217
  * append its request immediately before the failure hook lands; closing here
134
218
  * would let the later interaction poll reopen an already-failed Turn. */
135
219
  private turnFailurePending;
136
- /** Sub-agent (Task) ids already forwarded a Task tool_result is processed once. */
137
- private readonly seenSubagents;
220
+ /** Stop/StopFailure owns the hook cursor until its terminal status reaches a
221
+ * handled delivery outcome. This preserves replay on runner restart. */
222
+ private pendingTerminalHook?;
223
+ private terminalHookDelivery?;
224
+ private pendingTranscriptCompactions;
225
+ /** Claude Code native Task/Agent sidechains. Each child owns an independent
226
+ * cursor/retry lane so one unavailable delivery cannot block its siblings or
227
+ * the parent transcript. */
228
+ private readonly nativeSubagents;
229
+ private readonly nativeSubagentParentResponses;
230
+ private subagentStateParentPath?;
138
231
  /** The agent's task list, keyed by task id in creation order (claude
139
232
  * `TaskCreate`/`TaskUpdate`). Emitted whole as a snapshot on any change. */
140
233
  private readonly todos;
@@ -142,6 +235,7 @@ export declare class ClaudeLiveSession {
142
235
  * a turn's usage on close. undefined until the statusLine hook first fires. */
143
236
  private latestStatus?;
144
237
  private deltasOffset;
238
+ private readonly seenDeltaKeys;
145
239
  /** Finalized MessageDisplay ids in completion order, FIFO-mapped onto the
146
240
  * transcript's assistant-text records so a streamed message and its final
147
241
  * item share an itemId (message_id is absent from the transcript). */
@@ -179,22 +273,54 @@ export declare class ClaudeLiveSession {
179
273
  private consumeQueuedPromotion;
180
274
  start(): void;
181
275
  stop(): void;
276
+ /** Wait for the supervised polling task to retire. Terminal teardown starts
277
+ * synchronously in {@link stop}; callers use this bounded join before their
278
+ * runner process exits. */
279
+ waitForStop(timeoutMs?: number): Promise<void>;
182
280
  /** Phase two of runner shutdown. The caller must stop the Claude terminal
183
281
  * first so no hook can still be opening an atomically claimed answer. This is
184
282
  * synchronous because the runner process exits immediately afterwards. */
185
283
  finalizeStop(): void;
186
- /** One poll cycle (hooks → transcript → idle backstop). Exposed for tests to
284
+ /** One poll cycle (rotation/PreCompact prescan → transcript → hooks live
285
+ * lanes → idle backstop). Exposed for tests to
187
286
  * drive deterministically; the async {@link loop} just calls it on an interval. */
188
287
  tick(): void;
288
+ /** Bind Claude's per-process session metadata after the terminal exists. */
289
+ attachStatusSource({ panePid, configDir, }: {
290
+ panePid: () => number | undefined;
291
+ configDir?: string;
292
+ }): void;
293
+ private supervise;
189
294
  private loop;
295
+ private sleepUntilWake;
296
+ /** Rotation is a control edge, not an ordinary hook-stream item. Scan past a
297
+ * held Stop/compaction cursor so an old retry can never strand the physical
298
+ * Claude process on a new Session while Rynx still targets the old one. */
299
+ private prescanSessionRotation;
300
+ /** Mint compaction tokens before transcript parsing without advancing the
301
+ * hook cursor. The ordinary hook pass later observes the same cursor-keyed
302
+ * edge and converges on the already-created token. */
303
+ private prescanPrecompactEdges;
190
304
  private pollHooks;
305
+ private isSessionRotationHook;
306
+ private isForkHookRecord;
307
+ private persistHookCursor;
191
308
  private handleHook;
309
+ private handleTodoHook;
310
+ private compactionGeneration;
311
+ private readCompactionState;
312
+ private notePrecompact;
313
+ private compactionPendingMatches;
314
+ private markCompactionPersisted;
315
+ private claimHookCompaction;
316
+ private handleCompactionCompletionHook;
192
317
  /** Process a provider failure only after this tick has discovered every
193
318
  * blocking request already appended by its hook subprocess. */
194
319
  private flushTurnFailure;
195
320
  /** Emit the Stop idle signal only after this tick has discovered native
196
321
  * interactions. A pending question/permission is active execution, not idle. */
197
322
  private flushStopSignal;
323
+ private finishTerminalHookDelivery;
198
324
  /** SessionStart drives discovery (first) and rotation (a later one with a NEW
199
325
  * session id + transcript). `/clear` → source="clear"; `/fork` → source="resume"
200
326
  * into an unseen id with a forkedFrom marker. Any other new-transcript
@@ -204,7 +330,18 @@ export declare class ClaudeLiveSession {
204
330
  * files — hooks/deltas/status — are shared by the same claude process across a
205
331
  * rotation, so their cursors are NOT reset). */
206
332
  private repointTranscript;
333
+ /** Revalidate the committed cursor every poll, not only after process restart.
334
+ * Claude may replace/truncate a transcript in place. Treat a stale
335
+ * nonzero fingerprint as a new file and skips to its current EOF while
336
+ * preserving the bounded seen set. A temporarily missing file keeps the
337
+ * cursor unchanged. */
338
+ private validateLiveTranscriptCursor;
207
339
  private pollTranscript;
340
+ private rememberSourceId;
341
+ private restoreSubagentForwardStates;
342
+ private persistSubagentForwardStates;
343
+ private pollNativeSubagents;
344
+ private pollNativeSubagent;
208
345
  /** Tail blocking hook requests after transcript records, so an interaction
209
346
  * emitted in the same poll attaches to the user Turn that caused it. */
210
347
  private pollInteractions;
@@ -238,15 +375,16 @@ export declare class ClaudeLiveSession {
238
375
  private scheduleClaimedInteractionScrubs;
239
376
  private settleCancelledInteraction;
240
377
  private rememberSettled;
378
+ /** State change represented by this record once its durable terminal item is
379
+ * handled. Parsing can run through multiple records before their deliveries
380
+ * settle, so this checkpoint is intentionally separate from the live state. */
381
+ private terminalCommandCheckpointForRecord;
241
382
  private handleRecord;
383
+ private emitUserMessageRecord;
384
+ private handleCompactSummary;
242
385
  /** Fold a `TaskCreate` (new pending task) or `TaskUpdate` (status/subject/delete)
243
386
  * record into the task list; on any change emit the whole list as a snapshot. */
244
387
  private maybeUpdateTodos;
245
- /** On a `Task` tool_result, replay the sub-agent's own transcript
246
- * (`subagents/agent-<agentId>.jsonl`) as events tagged with the parent Task
247
- * tool-use id, so the canonical layer nests them under that call. Fired once
248
- * per sub-agent (at tool_result time the file is complete — no live race). */
249
- private maybeForwardSubagent;
250
388
  /** Tail streamed assistant-text chunks (MessageDisplay) into live `token`
251
389
  * events. Guarded on an open turn — deltas belong to the turn the user record
252
390
  * opened; the offset advances only once processed. */
@@ -255,6 +393,8 @@ export declare class ClaudeLiveSession {
255
393
  /** Refresh the latest statusLine context/cost snapshot (a last-writer-wins file
256
394
  * the statusLine hook overwrites on every TUI render). */
257
395
  private pollStatus;
396
+ private pollSessionStatus;
397
+ private handleSessionStatus;
258
398
  /** A usage record from the latest statusLine snapshot (snake_case, the keys the
259
399
  * normalizer's `turn_completed` + the web `readUsageTokens` read), or undefined
260
400
  * if the statusLine hook has not fired yet. */
@@ -265,10 +405,12 @@ export declare class ClaudeLiveSession {
265
405
  * text so a late MessageDisplay final can be reconciled backward. */
266
406
  private remapMessageItem;
267
407
  private trackTool;
408
+ private rememberNativeSubagentParentResponse;
268
409
  private ensureTurn;
269
- /** Mirror a local `!` command as its own mini-turn: close any open turn, then
270
- * open→emit→close so it groups as one response with a stable id (the record
271
- * uuid) and never lingers "running" (input+output are complete in one record). */
410
+ /** Mirror a Claude `!cmd` as one logical mini-turn even when input and output
411
+ * are separate transcript records. The output repeats the remembered command
412
+ * in Rynx's combined terminal item shape; the transcript projection merges
413
+ * that completion into the preceding command row. */
272
414
  private emitTerminalCommand;
273
415
  /** The web Stop button interrupted this session (host sent Escape). claude
274
416
  * records the interrupt in its own transcript but may not fire a Stop hook when
@@ -283,6 +425,10 @@ export declare class ClaudeLiveSession {
283
425
  * sent an Escape — an Escape into idle claude submits an empty turn, which
284
426
  * claude answers with a stray "No response requested." bubble. */
285
427
  isTurnOpen(): boolean;
428
+ /** Fail an open turn exactly once when its native terminal/runner disappears. */
429
+ failOpenTurn(error: Error): boolean;
430
+ /** Retire process-scoped metadata before classifying a terminal exit. */
431
+ noteTerminalExit(error: Error): boolean;
286
432
  private closeTurn;
287
433
  private closeTurnError;
288
434
  private resetMessageCorrelation;
@@ -292,6 +438,8 @@ export declare class ClaudeLiveSession {
292
438
  * {@link import("../terminal/tmux.js").TmuxTerminal}). The runner-child hands
293
439
  * this to the host after launching the pane, since the host doesn't own tmux. */
294
440
  export interface TerminalInjector {
441
+ /** PID of the process owning the tmux pane, when available. */
442
+ panePid?(): number | undefined;
295
443
  capturePane(): string;
296
444
  clearInputLine(): void;
297
445
  paste(text: string): void;
@@ -303,12 +451,16 @@ export interface InjectViaTerminalOptions {
303
451
  promptGlyph?: string;
304
452
  promptTimeoutMs?: number;
305
453
  settleMs?: number;
454
+ /** Wait for the pasted draft to become visible before the first Enter. */
455
+ pasteCommitMs?: number;
306
456
  /** Transcript-backed proof that Claude accepted this exact input. When
307
- * absent, injection keeps the legacy one-Enter best-effort contract. */
457
+ * present it takes precedence over pane heuristics. */
308
458
  submissionObserved?: () => boolean;
309
- /** Per-attempt wait for a transcript acknowledgement. */
459
+ /** Total post-Enter verification budget. */
310
460
  submitConfirmMs?: number;
311
- /** Total Enter attempts when transcript confirmation is available. */
461
+ /** Minimum delay between Enter retries while the draft remains visible. */
462
+ submitRetryMs?: number;
463
+ /** Optional safety cap for tests/callers; the time budget remains authoritative. */
312
464
  maxSubmitAttempts?: number;
313
465
  pollMs?: number;
314
466
  now?: () => number;
@@ -324,8 +476,8 @@ export interface InjectViaTerminalOptions {
324
476
  *
325
477
  * THROWS if the prompt never appears within the ready-gate window (reference implementation
326
478
  * `_wait_for_claude_prompt_ready` RAISE) — a not-ready pane is a hard error the
327
- * caller reports, NOT a signal to fall through to a second output path. With a
328
- * transcript observer, Enter is retried only when Claude has durably recorded
329
- * neither a direct user prompt nor a type-ahead enqueue.
479
+ * caller reports, NOT a signal to fall through to a second output path. Enter
480
+ * is retried only while the exact draft remains visible and Claude has durably
481
+ * recorded neither a direct user prompt nor a type-ahead enqueue.
330
482
  */
331
483
  export declare function injectViaTerminal(injector: TerminalInjector, text: string, opts?: InjectViaTerminalOptions): Promise<boolean>;