@rynx-ai/runtime 0.1.11-beta.37 → 0.1.11-beta.39

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.
@@ -4,16 +4,46 @@ import { type ClaudeRunnerStatus } from "./session-status.js";
4
4
  /** Mirror sink — the same shape as `CodexForwarderSink` so the host reuses one
5
5
  * per-turn normalizer wiring for both runtimes. */
6
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[]>;
7
20
  /** A turn began; `turnId` (the user record uuid) derives a stable responseId. */
8
21
  onTurnStart(turnId?: string): void;
9
22
  /** The user's prompt text (a `role:user` conversation message — not identity). */
10
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;
11
36
  /** A local shell command the user ran in the TUI (claude `!` bash mode), framed
12
37
  * as its own mini-turn (an `onTurnStart`/`onTurnEnd` pair brackets this call). */
13
38
  onTerminalCommand(cmd: TerminalCommandData): void;
14
39
  /** The agent's task list changed (claude `TaskCreate`/`TaskUpdate`) — the WHOLE
15
40
  * current list (a session-level snapshot, not tied to a turn). */
16
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>;
17
47
  /** One mapped event within the current turn. */
18
48
  onEvent(event: AgentEvent): void;
19
49
  /** A provider-neutral question/permission requested or settled while this
@@ -21,7 +51,7 @@ export interface ClaudeForwarderSink {
21
51
  onInteraction(event: RuntimeInteractionEvent): void;
22
52
  /** The current turn finished (a new user prompt, or the inactivity backstop).
23
53
  * `usage` carries the latest statusLine context/cost snapshot, when captured. */
24
- onTurnEnd(usage?: Record<string, unknown>, backgroundTaskCount?: number): void;
54
+ onTurnEnd(usage?: Record<string, unknown>, backgroundTaskCount?: number, reason?: "rotation"): void;
25
55
  /** The current turn ended because the user explicitly interrupted it. */
26
56
  onTurnInterrupted?(usage?: Record<string, unknown>): void;
27
57
  /** Escape was sent for the open Turn. Publish cancelled UI state immediately;
@@ -34,10 +64,10 @@ export interface ClaudeForwarderSink {
34
64
  * assistant record and the two orderings race; finalizing here would split a
35
65
  * late assistant record into its own turn. The turn is finalized by the next
36
66
  * user prompt or the inactivity backstop, so a late record still joins it. */
37
- onIdle(backgroundTaskCount?: number): void;
67
+ onIdle(backgroundTaskCount?: number, sourceId?: string): void | Promise<void>;
38
68
  /** Claude's current Turn or native runtime failed (StopFailure/pane exit).
39
69
  * May fire between Turns so Session-level failure can retire stale liveness. */
40
- onTurnError(error: Error): void;
70
+ onTurnError(error: Error, sourceId?: string): void | Promise<void>;
41
71
  /** Fired once SessionStart reveals claude's session id + transcript path, so
42
72
  * the host can persist the id and release its readiness gate. */
43
73
  onSessionDiscovered?(claudeSessionId: string, transcriptPath: string): void;
@@ -45,10 +75,10 @@ export interface ClaudeForwarderSink {
45
75
  * binding. The host keeps the original binding and fails readiness. */
46
76
  onSessionResumeError?(error: Error): void;
47
77
  /** The claude session rotated (`/clear` → fresh, `/fork` → derived): a new
48
- * SessionStart reported a new session id + transcript. The forwarder has
49
- * already re-pointed to the new transcript and reset its per-session state; the
50
- * host mints a fresh rynx session and re-targets the mirror to it. */
51
- 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>;
52
82
  }
53
83
  export interface ClaudeLiveSessionOptions {
54
84
  /** The session's bridge dir (`hooks.jsonl` lives here). */
@@ -64,6 +94,10 @@ export interface ClaudeLiveSessionOptions {
64
94
  * matching forwarder cursor exists, follow only records appended after the
65
95
  * SessionStart discovery instead of mirroring native history a second time. */
66
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;
67
101
  /** Poll interval (ms). The files are append-only, so polling is simplest. */
68
102
  pollMs?: number;
69
103
  /** Inactivity (ms) FALLBACK close for a turn whose Stop hook never fired. */
@@ -92,9 +126,24 @@ export declare class ClaudeLiveSession {
92
126
  private releaseSleep?;
93
127
  private hooksOffset;
94
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?;
95
137
  private interactionsOffset;
96
138
  private interactionAcksOffset;
97
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;
98
147
  /** Secondary dedup: source ids (record uuids) already forwarded, so a re-read
99
148
  * (fingerprint reset / mid-poll death) doesn't re-emit. Persisted in the
100
149
  * forwarder state; bounded there. */
@@ -106,11 +155,20 @@ export declare class ClaudeLiveSession {
106
155
  /** Persisted native id requested through `claude --resume`. */
107
156
  private readonly expectedClaudeSessionId?;
108
157
  private readonly resumeAtEndOnDiscovery;
158
+ private readonly initialTranscriptOffsetOnDiscovery;
109
159
  /** Every claude session uuid seen — a resume into an UNSEEN id + a forkedFrom
110
160
  * marker signals a `/fork` (vs. resuming a known branch). */
111
161
  private readonly seenClaudeSessionIds;
112
162
  private turnOpen;
113
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?;
114
172
  /** The open turn received an explicit Escape/Stop and must close cancelled. */
115
173
  private turnInterrupted;
116
174
  /** The Turn opened from a pre-transcript interaction. Its unique provisional
@@ -159,8 +217,17 @@ export declare class ClaudeLiveSession {
159
217
  * append its request immediately before the failure hook lands; closing here
160
218
  * would let the later interaction poll reopen an already-failed Turn. */
161
219
  private turnFailurePending;
162
- /** Sub-agent (Task) ids already forwarded a Task tool_result is processed once. */
163
- 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?;
164
231
  /** The agent's task list, keyed by task id in creation order (claude
165
232
  * `TaskCreate`/`TaskUpdate`). Emitted whole as a snapshot on any change. */
166
233
  private readonly todos;
@@ -168,6 +235,7 @@ export declare class ClaudeLiveSession {
168
235
  * a turn's usage on close. undefined until the statusLine hook first fires. */
169
236
  private latestStatus?;
170
237
  private deltasOffset;
238
+ private readonly seenDeltaKeys;
171
239
  /** Finalized MessageDisplay ids in completion order, FIFO-mapped onto the
172
240
  * transcript's assistant-text records so a streamed message and its final
173
241
  * item share an itemId (message_id is absent from the transcript). */
@@ -213,7 +281,8 @@ export declare class ClaudeLiveSession {
213
281
  * first so no hook can still be opening an atomically claimed answer. This is
214
282
  * synchronous because the runner process exits immediately afterwards. */
215
283
  finalizeStop(): void;
216
- /** 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
217
286
  * drive deterministically; the async {@link loop} just calls it on an interval. */
218
287
  tick(): void;
219
288
  /** Bind Claude's per-process session metadata after the terminal exists. */
@@ -224,15 +293,34 @@ export declare class ClaudeLiveSession {
224
293
  private supervise;
225
294
  private loop;
226
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;
227
304
  private pollHooks;
305
+ private isSessionRotationHook;
306
+ private isForkHookRecord;
228
307
  private persistHookCursor;
229
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;
230
317
  /** Process a provider failure only after this tick has discovered every
231
318
  * blocking request already appended by its hook subprocess. */
232
319
  private flushTurnFailure;
233
320
  /** Emit the Stop idle signal only after this tick has discovered native
234
321
  * interactions. A pending question/permission is active execution, not idle. */
235
322
  private flushStopSignal;
323
+ private finishTerminalHookDelivery;
236
324
  /** SessionStart drives discovery (first) and rotation (a later one with a NEW
237
325
  * session id + transcript). `/clear` → source="clear"; `/fork` → source="resume"
238
326
  * into an unseen id with a forkedFrom marker. Any other new-transcript
@@ -242,7 +330,18 @@ export declare class ClaudeLiveSession {
242
330
  * files — hooks/deltas/status — are shared by the same claude process across a
243
331
  * rotation, so their cursors are NOT reset). */
244
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;
245
339
  private pollTranscript;
340
+ private rememberSourceId;
341
+ private restoreSubagentForwardStates;
342
+ private persistSubagentForwardStates;
343
+ private pollNativeSubagents;
344
+ private pollNativeSubagent;
246
345
  /** Tail blocking hook requests after transcript records, so an interaction
247
346
  * emitted in the same poll attaches to the user Turn that caused it. */
248
347
  private pollInteractions;
@@ -276,15 +375,16 @@ export declare class ClaudeLiveSession {
276
375
  private scheduleClaimedInteractionScrubs;
277
376
  private settleCancelledInteraction;
278
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;
279
382
  private handleRecord;
383
+ private emitUserMessageRecord;
384
+ private handleCompactSummary;
280
385
  /** Fold a `TaskCreate` (new pending task) or `TaskUpdate` (status/subject/delete)
281
386
  * record into the task list; on any change emit the whole list as a snapshot. */
282
387
  private maybeUpdateTodos;
283
- /** On a `Task` tool_result, replay the sub-agent's own transcript
284
- * (`subagents/agent-<agentId>.jsonl`) as events tagged with the parent Task
285
- * tool-use id, so the canonical layer nests them under that call. Fired once
286
- * per sub-agent (at tool_result time the file is complete — no live race). */
287
- private maybeForwardSubagent;
288
388
  /** Tail streamed assistant-text chunks (MessageDisplay) into live `token`
289
389
  * events. Guarded on an open turn — deltas belong to the turn the user record
290
390
  * opened; the offset advances only once processed. */
@@ -305,10 +405,12 @@ export declare class ClaudeLiveSession {
305
405
  * text so a late MessageDisplay final can be reconciled backward. */
306
406
  private remapMessageItem;
307
407
  private trackTool;
408
+ private rememberNativeSubagentParentResponse;
308
409
  private ensureTurn;
309
- /** Mirror a local `!` command as its own mini-turn: close any open turn, then
310
- * open→emit→close so it groups as one response with a stable id (the record
311
- * 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. */
312
414
  private emitTerminalCommand;
313
415
  /** The web Stop button interrupted this session (host sent Escape). claude
314
416
  * records the interrupt in its own transcript but may not fire a Stop hook when