Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
reports directly, not by re-parsing the parent-facing report's prose.
Removes the `isXxxSubAgentReport` classifier family and per-reason parent
hint functions in favor of a single structured switch.
### Internal

- Removed the dead Ink-era kill ring copy (`src/tui/kill-ring.ts`); the OpenTUI
prompt kill ring (`src/tui/prompt-kill-ring.ts`) is the sole implementation.
- Extracted the shared timeout-race helper (`src/util/budget-race.ts`) used by
the shell-guard search budget and the tool-execution watchdog, replacing two
independent copies of the same `AbortController` + `setTimeout` race.
- `runtime-bridge.ts` now re-exports `mapReactorLike` from `stream-event-map.ts`
instead of wrapping it in an identical local function.

## [0.2.108] - 2026-08-24

Expand Down
37 changes: 3 additions & 34 deletions src/plugins/shell-guard-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { spawn, type ChildProcess } from "node:child_process";
import { realpathSync } from "node:fs";
import type { ToolPlugin } from "@intx/tools-posix";
import { formatSearchTimeoutMessage } from "./tool-time-budget.js";
import { formatSearchTimeoutMessage, TIMEOUT_PREFIX } from "./tool-time-budget.js";
import { BUDGET_EXPIRED, budgetExpiry, withTimeout } from "../util/budget-race.js";
import type { ToolDefinition } from "@intx/types/runtime";
import {
assertShellCwdUsable,
Expand Down Expand Up @@ -325,38 +326,6 @@ function optionalNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}

function withTimeout(
signal: AbortSignal,
timeoutMs: number,
): { signal: AbortSignal; dispose: () => void } {
const controller = new AbortController();
const onParentAbort = () => controller.abort();
signal.addEventListener("abort", onParentAbort, { once: true });
const timer = setTimeout(() => controller.abort(), timeoutMs);
if (signal.aborted) controller.abort();
return {
signal: controller.signal,
dispose: () => {
clearTimeout(timer);
signal.removeEventListener("abort", onParentAbort);
},
};
}

const BUDGET_EXPIRED = Symbol("search-budget-expired");

function budgetExpiry(signal: AbortSignal): Promise<typeof BUDGET_EXPIRED> {
return new Promise((resolve) => {
if (signal.aborted) {
resolve(BUDGET_EXPIRED);
return;
}
signal.addEventListener("abort", () => resolve(BUDGET_EXPIRED), {
once: true,
});
});
}

/**
* Replaces stock run_shell with a hard-capped implementation, and applies a
* 10s wall-clock budget to grep/search_files when the agent does not abort
Expand Down Expand Up @@ -510,7 +479,7 @@ export function shellGuardPlugin(
if (
outcome.isError === true &&
typeof outcome.content === "string" &&
outcome.content.includes("[timed out before completing]")
outcome.content.includes(TIMEOUT_PREFIX)
) {
return outcome;
}
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/tool-time-budget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

const TIMEOUT_PREFIX = "[timed out before completing]";
export const TIMEOUT_PREFIX = "[timed out before completing]";

export function scopedSearchRetryHints(tool: ScopedSearchTool): string {
const base =
Expand Down
134 changes: 0 additions & 134 deletions src/tui/kill-ring.test.ts

This file was deleted.

100 changes: 0 additions & 100 deletions src/tui/kill-ring.ts

This file was deleted.

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

export const KILL_RING_MAX = 10;
Expand Down
13 changes: 3 additions & 10 deletions src/tui/runtime-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,15 @@ import {
PRODUCTION_REACTOR_TYPES,
createStreamMapContext,
mapProductionEvent,
mapReactorLike as mapReactorLikeImpl,
mapReactorLike,
type BridgeInboundEvent,
type ReactorLikeEvent,
type StreamMapContext,
} from "./stream-event-map.js";

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

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

/**
* Map a reactor-like event into zero or more canonical bridge events.
* Stateless (fixture-friendly). Live sessions use a StreamMapContext via handle.
*/
export function mapReactorLike(event: ReactorLikeEvent): readonly BridgeInboundEvent[] {
return mapReactorLikeImpl(event);
}

function rowFromInbound(event: BridgeInboundEvent): StreamRow | null {
switch (event.type) {
case "user":
Expand Down
Loading
Loading