@rynx-ai/runtime 0.1.11-beta.3 → 0.1.11-beta.30

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 (44) 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 +2 -0
  6. package/dist/claude/native-bridge.js +23 -0
  7. package/dist/claude/native-hook-main.js +62 -0
  8. package/dist/claude/native-integration.d.ts +50 -10
  9. package/dist/claude/native-integration.js +262 -37
  10. package/dist/claude/session-status.d.ts +39 -0
  11. package/dist/claude/session-status.js +163 -0
  12. package/dist/claude/transcript.js +27 -17
  13. package/dist/codex-app-server/client.d.ts +10 -6
  14. package/dist/codex-app-server/client.js +67 -15
  15. package/dist/codex-app-server/forwarder.d.ts +92 -3
  16. package/dist/codex-app-server/forwarder.js +509 -56
  17. package/dist/codex-app-server/mapping.d.ts +3 -6
  18. package/dist/codex-app-server/mapping.js +174 -28
  19. package/dist/codex-app-server/mcp-startup.d.ts +13 -0
  20. package/dist/codex-app-server/mcp-startup.js +63 -0
  21. package/dist/codex-app-server/protocol.d.ts +64 -7
  22. package/dist/codex-app-server/ws-channel.js +19 -19
  23. package/dist/codex-home.js +2 -4
  24. package/dist/host.d.ts +64 -21
  25. package/dist/host.js +1330 -441
  26. package/dist/index.d.ts +1 -1
  27. package/dist/input-resources.d.ts +4 -0
  28. package/dist/input-resources.js +21 -5
  29. package/dist/models-catalog.d.ts +2 -1
  30. package/dist/models-catalog.js +94 -6
  31. package/dist/runner/child.d.ts +48 -21
  32. package/dist/runner/child.js +550 -48
  33. package/dist/runner/manager.d.ts +54 -13
  34. package/dist/runner/manager.js +479 -114
  35. package/dist/runner/protocol.d.ts +62 -19
  36. package/dist/runner/protocol.js +5 -0
  37. package/dist/runner/startup-policy.d.ts +7 -0
  38. package/dist/runner/startup-policy.js +10 -0
  39. package/dist/terminal/claude-tui.d.ts +3 -1
  40. package/dist/terminal/claude-tui.js +3 -1
  41. package/dist/terminal/registry.js +3 -2
  42. package/dist/terminal/tmux.d.ts +50 -7
  43. package/dist/terminal/tmux.js +168 -47
  44. package/package.json +4 -3
@@ -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
- * Materialize a selected catalog skill subset into a throwaway claude
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 materializeSkillPlugin(selected: SkillMeta[] | null | undefined): Promise<{
13
- pluginDir?: string;
14
- cleanup?: () => Promise<void>;
15
- } | null>;
29
+ export declare function materializeClaudePlugin(sessionId: string, selected: SkillMeta[] | null | undefined): Promise<MaterializedClaudePlugin | null>;
@@ -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
- * Materialize a selected catalog skill subset into a throwaway claude
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 materializeSkillPlugin(selected) {
38
+ export async function materializeClaudePlugin(sessionId, selected) {
15
39
  if (selected == null || selected.length === 0)
16
40
  return null;
17
- const dir = await mkdtemp(path.join(tmpdir(), "rynx-claude-skills-"));
18
- const skillsRoot = path.join(dir, "skills");
19
- await mkdir(skillsRoot, { recursive: true });
20
- for (const skill of selected) {
21
- await cp(skill.dir, path.join(skillsRoot, skill.name), { recursive: true });
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
  }
@@ -1,9 +1,4 @@
1
1
  import type { ModelInfo, ModelListResponse } from "../codex-app-server/protocol.js";
2
- /**
3
- * Default model for the claude runtime when `CLAUDE_MODEL` is unset. Kept in
4
- * sync with the `CLAUDE_MODEL` default in {@link import("../config.js")}.
5
- */
6
- export declare const CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-6";
7
2
  /** Build the `ModelInfo[]` for the claude runtime. */
8
3
  export declare function claudeModelInfos(): ModelInfo[];
9
4
  /** {@link ModelListResponse} for the claude runtime (broker `listModels`). */
@@ -1,8 +1,3 @@
1
- /**
2
- * Default model for the claude runtime when `CLAUDE_MODEL` is unset. Kept in
3
- * sync with the `CLAUDE_MODEL` default in {@link import("../config.js")}.
4
- */
5
- export const CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-6";
6
1
  /**
7
2
  * Static catalogue of Claude models the `/model` card and `/models` reply offer
8
3
  * for the claude runtime. Unlike codex/traex (which query a live app-server),
@@ -12,7 +7,7 @@ export const CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-6";
12
7
  */
13
8
  const CLAUDE_MODEL_IDS = [
14
9
  { id: "claude-opus-4-8", displayName: "Claude Opus 4.8 · 最强" },
15
- { id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6 · 均衡(默认)" },
10
+ { id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6 · 均衡" },
16
11
  { id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5 · 最快" },
17
12
  { id: "claude-opus-4-7", displayName: "Claude Opus 4.7" },
18
13
  { id: "claude-opus-4-6", displayName: "Claude Opus 4.6" },
@@ -24,7 +19,6 @@ export function claudeModelInfos() {
24
19
  id,
25
20
  model: id,
26
21
  displayName,
27
- isDefault: id === CLAUDE_DEFAULT_MODEL,
28
22
  }));
29
23
  }
30
24
  /** {@link ModelListResponse} for the claude runtime (broker `listModels`). */
@@ -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)
@@ -1,5 +1,6 @@
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 {
@@ -20,14 +21,22 @@ export interface ClaudeForwarderSink {
20
21
  onInteraction(event: RuntimeInteractionEvent): void;
21
22
  /** The current turn finished (a new user prompt, or the inactivity backstop).
22
23
  * `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
24
+ onTurnEnd(usage?: Record<string, unknown>, backgroundTaskCount?: number): 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;
30
+ /** The runtime status changed according to Claude's session metadata. */
31
+ onStatus?(status: ClaudeRunnerStatus, blockedOn?: string): void;
32
+ /** The runtime went idle on the legacy hook fallback — surface idle WITHOUT finalizing
25
33
  * the turn. claude fires Stop around the same time it flushes the final
26
34
  * assistant record and the two orderings race; finalizing here would split a
27
35
  * late assistant record into its own turn. The turn is finalized by the next
28
36
  * 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). */
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. */
31
40
  onTurnError(error: Error): void;
32
41
  /** Fired once SessionStart reveals claude's session id + transcript path, so
33
42
  * the host can persist the id and release its readiness gate. */
@@ -99,12 +108,26 @@ export declare class ClaudeLiveSession {
99
108
  private readonly seenClaudeSessionIds;
100
109
  private turnOpen;
101
110
  private currentTurnId?;
111
+ /** The open turn received an explicit Escape/Stop and must close cancelled. */
112
+ private turnInterrupted;
102
113
  /** The Turn opened from a pre-transcript interaction. Its unique provisional
103
114
  * id keeps responses distinct until the real prompt uuid can be adopted. */
104
115
  private syntheticTurn;
105
116
  private lastActivityAt;
106
117
  /** When the Stop hook fired (null = not yet) — drives the grace-period close. */
107
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;
123
+ /** An idle status from Claude's session metadata. The short delay keeps a
124
+ * final transcript record in the response without letting stale tool state
125
+ * override the provider's terminal status. */
126
+ private providerIdleAt;
127
+ private statusPoller?;
128
+ /** A failed hook is authoritative until the next running edge. Claude writes
129
+ * idle after failures too, so that trailing file update must not erase it. */
130
+ private providerFailureSticky;
108
131
  /** Open tool-call ids (start seen, end not yet) — suppresses the idle backstop. */
109
132
  private readonly openToolIds;
110
133
  /** Transcript-backed acknowledgements for web→TUI input. A direct idle
@@ -186,6 +209,11 @@ export declare class ClaudeLiveSession {
186
209
  /** One poll cycle (hooks → transcript → idle backstop). Exposed for tests to
187
210
  * drive deterministically; the async {@link loop} just calls it on an interval. */
188
211
  tick(): void;
212
+ /** Bind Claude's per-process session metadata after the terminal exists. */
213
+ attachStatusSource({ panePid, configDir, }: {
214
+ panePid: () => number | undefined;
215
+ configDir?: string;
216
+ }): void;
189
217
  private loop;
190
218
  private pollHooks;
191
219
  private handleHook;
@@ -255,6 +283,8 @@ export declare class ClaudeLiveSession {
255
283
  /** Refresh the latest statusLine context/cost snapshot (a last-writer-wins file
256
284
  * the statusLine hook overwrites on every TUI render). */
257
285
  private pollStatus;
286
+ private pollSessionStatus;
287
+ private handleSessionStatus;
258
288
  /** A usage record from the latest statusLine snapshot (snake_case, the keys the
259
289
  * normalizer's `turn_completed` + the web `readUsageTokens` read), or undefined
260
290
  * if the statusLine hook has not fired yet. */
@@ -283,6 +313,10 @@ export declare class ClaudeLiveSession {
283
313
  * sent an Escape — an Escape into idle claude submits an empty turn, which
284
314
  * claude answers with a stray "No response requested." bubble. */
285
315
  isTurnOpen(): boolean;
316
+ /** Fail an open turn exactly once when its native terminal/runner disappears. */
317
+ failOpenTurn(error: Error): boolean;
318
+ /** Retire process-scoped metadata before classifying a terminal exit. */
319
+ noteTerminalExit(error: Error): boolean;
286
320
  private closeTurn;
287
321
  private closeTurnError;
288
322
  private resetMessageCorrelation;
@@ -292,6 +326,8 @@ export declare class ClaudeLiveSession {
292
326
  * {@link import("../terminal/tmux.js").TmuxTerminal}). The runner-child hands
293
327
  * this to the host after launching the pane, since the host doesn't own tmux. */
294
328
  export interface TerminalInjector {
329
+ /** PID of the process owning the tmux pane, when available. */
330
+ panePid?(): number | undefined;
295
331
  capturePane(): string;
296
332
  clearInputLine(): void;
297
333
  paste(text: string): void;
@@ -303,12 +339,16 @@ export interface InjectViaTerminalOptions {
303
339
  promptGlyph?: string;
304
340
  promptTimeoutMs?: number;
305
341
  settleMs?: number;
342
+ /** Wait for the pasted draft to become visible before the first Enter. */
343
+ pasteCommitMs?: number;
306
344
  /** Transcript-backed proof that Claude accepted this exact input. When
307
- * absent, injection keeps the legacy one-Enter best-effort contract. */
345
+ * present it takes precedence over pane heuristics. */
308
346
  submissionObserved?: () => boolean;
309
- /** Per-attempt wait for a transcript acknowledgement. */
347
+ /** Total post-Enter verification budget. */
310
348
  submitConfirmMs?: number;
311
- /** Total Enter attempts when transcript confirmation is available. */
349
+ /** Minimum delay between Enter retries while the draft remains visible. */
350
+ submitRetryMs?: number;
351
+ /** Optional safety cap for tests/callers; the time budget remains authoritative. */
312
352
  maxSubmitAttempts?: number;
313
353
  pollMs?: number;
314
354
  now?: () => number;
@@ -324,8 +364,8 @@ export interface InjectViaTerminalOptions {
324
364
  *
325
365
  * THROWS if the prompt never appears within the ready-gate window (reference implementation
326
366
  * `_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.
367
+ * caller reports, NOT a signal to fall through to a second output path. Enter
368
+ * is retried only while the exact draft remains visible and Claude has durably
369
+ * recorded neither a direct user prompt nor a type-ahead enqueue.
330
370
  */
331
371
  export declare function injectViaTerminal(injector: TerminalInjector, text: string, opts?: InjectViaTerminalOptions): Promise<boolean>;