Skip to content

Commit e2ce1ca

Browse files
committed
Consolidate duplicate kill ring and timeout-race modules (CL-6790)
- Delete the dead Ink-era kill ring (src/tui/kill-ring.ts + test); the OpenTUI prompt-kill-ring.ts is the sole implementation. - Extract shared timeout-race primitives (src/util/budget-race.ts) used by shell-guard's search budget and the tool-execution watchdog, replacing two copies of the same AbortController+setTimeout race and BUDGET_EXPIRED sentinel; both now reference the shared timeout-prefix string constant instead of a hardcoded literal. - runtime-bridge.ts re-exports mapReactorLike from stream-event-map.ts instead of wrapping it in an identical local function.
1 parent 6c05612 commit e2ce1ca

9 files changed

Lines changed: 60 additions & 313 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ matching `## [X.Y.Z]` section (plus install instructions). Do not maintain
1111
parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
1212
`## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD`, then run the release script.
1313

14+
## [Unreleased]
15+
16+
### Internal
17+
18+
- Removed the dead Ink-era kill ring copy (`src/tui/kill-ring.ts`); the OpenTUI
19+
prompt kill ring (`src/tui/prompt-kill-ring.ts`) is the sole implementation.
20+
- Extracted the shared timeout-race helper (`src/util/budget-race.ts`) used by
21+
the shell-guard search budget and the tool-execution watchdog, replacing two
22+
independent copies of the same `AbortController` + `setTimeout` race.
23+
- `runtime-bridge.ts` now re-exports `mapReactorLike` from `stream-event-map.ts`
24+
instead of wrapping it in an identical local function.
25+
1426
## [0.2.108] - 2026-08-24
1527

1628
### Agent

src/plugins/shell-guard-plugin.ts

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { spawn, type ChildProcess } from "node:child_process";
22
import { realpathSync } from "node:fs";
33
import type { ToolPlugin } from "@intx/tools-posix";
4-
import { formatSearchTimeoutMessage } from "./tool-time-budget.js";
4+
import { formatSearchTimeoutMessage, TIMEOUT_PREFIX } from "./tool-time-budget.js";
5+
import { BUDGET_EXPIRED, budgetExpiry, withTimeout } from "../util/budget-race.js";
56
import type { ToolDefinition } from "@intx/types/runtime";
67
import {
78
assertShellCwdUsable,
@@ -325,38 +326,6 @@ function optionalNumber(value: unknown): number | undefined {
325326
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
326327
}
327328

328-
function withTimeout(
329-
signal: AbortSignal,
330-
timeoutMs: number,
331-
): { signal: AbortSignal; dispose: () => void } {
332-
const controller = new AbortController();
333-
const onParentAbort = () => controller.abort();
334-
signal.addEventListener("abort", onParentAbort, { once: true });
335-
const timer = setTimeout(() => controller.abort(), timeoutMs);
336-
if (signal.aborted) controller.abort();
337-
return {
338-
signal: controller.signal,
339-
dispose: () => {
340-
clearTimeout(timer);
341-
signal.removeEventListener("abort", onParentAbort);
342-
},
343-
};
344-
}
345-
346-
const BUDGET_EXPIRED = Symbol("search-budget-expired");
347-
348-
function budgetExpiry(signal: AbortSignal): Promise<typeof BUDGET_EXPIRED> {
349-
return new Promise((resolve) => {
350-
if (signal.aborted) {
351-
resolve(BUDGET_EXPIRED);
352-
return;
353-
}
354-
signal.addEventListener("abort", () => resolve(BUDGET_EXPIRED), {
355-
once: true,
356-
});
357-
});
358-
}
359-
360329
/**
361330
* Replaces stock run_shell with a hard-capped implementation, and applies a
362331
* 10s wall-clock budget to grep/search_files when the agent does not abort
@@ -510,7 +479,7 @@ export function shellGuardPlugin(
510479
if (
511480
outcome.isError === true &&
512481
typeof outcome.content === "string" &&
513-
outcome.content.includes("[timed out before completing]")
482+
outcome.content.includes(TIMEOUT_PREFIX)
514483
) {
515484
return outcome;
516485
}

src/plugins/tool-time-budget.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
export type ScopedSearchTool = "grep" | "search_files";
44

5-
const TIMEOUT_PREFIX = "[timed out before completing]";
5+
export const TIMEOUT_PREFIX = "[timed out before completing]";
66

77
export function scopedSearchRetryHints(tool: ScopedSearchTool): string {
88
const base =

src/tui/kill-ring.test.ts

Lines changed: 0 additions & 134 deletions
This file was deleted.

src/tui/kill-ring.ts

Lines changed: 0 additions & 100 deletions
This file was deleted.

src/tui/prompt-kill-ring.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515
* `rotateYank` hand back the text to splice in; shell.ts performs the splice
1616
* against the InputRenderable directly.
1717
*
18-
* Mirrors the semantics of src/tui/kill-ring.ts (the Ink reference) without
19-
* importing from it — the two prompt implementations are independent trees
20-
* during the OpenTUI cutover.
18+
* Sole kill ring implementation (the former Ink-era src/tui/kill-ring.ts
19+
* copy was retired once the OpenTUI cutover made it dead code).
2120
*/
2221

2322
export const KILL_RING_MAX = 10;

src/tui/runtime-bridge.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,15 @@ import {
8686
PRODUCTION_REACTOR_TYPES,
8787
createStreamMapContext,
8888
mapProductionEvent,
89-
mapReactorLike as mapReactorLikeImpl,
89+
mapReactorLike,
9090
type BridgeInboundEvent,
9191
type ReactorLikeEvent,
9292
type StreamMapContext,
9393
} from "./stream-event-map.js";
9494

95-
/** Re-export map types so existing `from "./runtime-bridge"` imports keep working. */
95+
/** Re-export map types/fn so existing `from "./runtime-bridge"` imports keep working. */
9696
export type { BridgeInboundEvent, ReactorLikeEvent, StreamMapContext };
97+
export { mapReactorLike };
9798

9899
/** Outbound actions the UI asks the session runtime to perform. */
99100
export interface SessionPort {
@@ -260,14 +261,6 @@ function isBridgeInbound(event: { type: string }): event is BridgeInboundEvent {
260261
}
261262
}
262263

263-
/**
264-
* Map a reactor-like event into zero or more canonical bridge events.
265-
* Stateless (fixture-friendly). Live sessions use a StreamMapContext via handle.
266-
*/
267-
export function mapReactorLike(event: ReactorLikeEvent): readonly BridgeInboundEvent[] {
268-
return mapReactorLikeImpl(event);
269-
}
270-
271264
function rowFromInbound(event: BridgeInboundEvent): StreamRow | null {
272265
switch (event.type) {
273266
case "user":

0 commit comments

Comments
 (0)