From 196550b7b6fe8babe6c5fb14b0ee653c81c97a0c Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 7 Oct 2026 22:55:32 -0700 Subject: [PATCH 1/3] fix(reactor): exempt idempotent task bookkeeping from doom-loop fail-run --- src/provider/inference-dependencies.ts | 9 +++- src/subagent/idempotent-task-exempt.test.ts | 59 +++++++++++++++++++++ src/subagent/idempotent-task-exempt.ts | 23 ++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 src/subagent/idempotent-task-exempt.test.ts create mode 100644 src/subagent/idempotent-task-exempt.ts diff --git a/src/provider/inference-dependencies.ts b/src/provider/inference-dependencies.ts index 9a28ad486..ee4b8c58b 100644 --- a/src/provider/inference-dependencies.ts +++ b/src/provider/inference-dependencies.ts @@ -19,6 +19,7 @@ import { GROK_RESPONSES_PROVIDER } from "./grok-responses.js"; import { withAnthropicCacheBreakpoint } from "./anthropic-cache-breakpoint.js"; import { withReplaySanitizer } from "./replay-sanitizer.js"; import { isPollOnlyPendingBatch } from "../subagent/poll-exempt.js"; +import { isIdempotentTaskBatch } from "../subagent/idempotent-task-exempt.js"; import { OPENCODE_GO_PROVIDER_ID } from "../../packages/opencode-go/src/index.js"; import { BIFROST_PROVIDER } from "./bifrost-adapter.js"; import { OPENAI_RESPONSES_PROVIDER } from "./openai-responses.js"; @@ -101,7 +102,13 @@ export function createInferenceDependencies(): Promise { .then((deps) => ({ ...deps, fetch: withCodexContentTypeRepair(deps.fetch), - isPollOnlyPendingBatch, + // Compose the first-party doom-loop exemptions into the single liveness + // hook the vendored reactor already consumes: still-pending polls and + // idempotent task bookkeeping both reset the repeat streak instead of + // counting toward the fail-run threshold. + isPollOnlyPendingBatch: (calls, results) => + isPollOnlyPendingBatch(calls, results) || + isIdempotentTaskBatch(calls, results), })); } return cached; diff --git a/src/subagent/idempotent-task-exempt.test.ts b/src/subagent/idempotent-task-exempt.test.ts new file mode 100644 index 000000000..f599d5f0e --- /dev/null +++ b/src/subagent/idempotent-task-exempt.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test"; +import type { ToolCall, ToolResult } from "@intx/types/runtime"; +import { isIdempotentTaskBatch } from "./idempotent-task-exempt.js"; + +function call(name: string, id = "c1"): ToolCall { + return { id, name, arguments: {} }; +} + +function taskResult(): ToolResult { + return { callId: "c1", content: "{}" }; +} + +describe("isIdempotentTaskBatch", () => { + test("manage_tasks batch is exempt", () => { + expect( + isIdempotentTaskBatch([call("manage_tasks")], [taskResult()]), + ).toBe(true); + }); + + test("todowrite alias is exempt", () => { + expect(isIdempotentTaskBatch([call("todowrite")], [taskResult()])).toBe( + true, + ); + }); + + test("update_plan hidden alias is exempt", () => { + expect(isIdempotentTaskBatch([call("update_plan")], [taskResult()])).toBe( + true, + ); + }); + + test("repeated task-only batches stay exempt", () => { + expect( + isIdempotentTaskBatch( + [call("todowrite", "c1"), call("todowrite", "c2")], + [taskResult(), { callId: "c2", content: "{}" }], + ), + ).toBe(true); + }); + + test("a real tool in the batch is never exempt", () => { + expect( + isIdempotentTaskBatch( + [call("todowrite"), call("run_shell")], + [taskResult(), { callId: "c2", content: "{}" }], + ), + ).toBe(false); + }); + + test("non-task tools are never exempt", () => { + expect( + isIdempotentTaskBatch([call("run_shell")], [taskResult()]), + ).toBe(false); + }); + + test("empty batches are never exempt", () => { + expect(isIdempotentTaskBatch([], [])).toBe(false); + }); +}); \ No newline at end of file diff --git a/src/subagent/idempotent-task-exempt.ts b/src/subagent/idempotent-task-exempt.ts new file mode 100644 index 000000000..2038052ce --- /dev/null +++ b/src/subagent/idempotent-task-exempt.ts @@ -0,0 +1,23 @@ +import { isSameTool } from "../agent/canonical-tool-name.js"; +import type { ToolCall, ToolResult } from "@intx/types/runtime"; + +/** + * Doom-loop exemption for idempotent task bookkeeping. A batch is exempt only + * when it is non-empty and every executed call is a task-bookkeeping tool + * (`manage_tasks` — `isSameTool` folds the `todowrite` and `update_plan` + * aliases onto that engine id). + * + * Repeating an identical task update is legitimate bookkeeping, not a runaway + * loop: "finalize checklist" three times changes nothing and should not + * abort an otherwise-finished run under the `fail-run` policy. This resets + * the doom-loop streak (same mechanism as the poll exemption) so consecutive + * task-only batches never accumulate toward the threshold, while any real + * tool in the batch counts normally and always preserves the guard. + */ +export function isIdempotentTaskBatch( + calls: readonly ToolCall[], + _results: readonly ToolResult[], +): boolean { + if (calls.length === 0) return false; + return calls.every((call) => isSameTool(call.name, "manage_tasks")); +} \ No newline at end of file From 690f6dc7f77024e253e80a8c415fb3ee4d2cef3c Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 8 Oct 2026 13:48:54 -0700 Subject: [PATCH 2/3] style(reactor): format idempotent task exemption files --- src/subagent/idempotent-task-exempt.test.ts | 14 +++++++------- src/subagent/idempotent-task-exempt.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/subagent/idempotent-task-exempt.test.ts b/src/subagent/idempotent-task-exempt.test.ts index f599d5f0e..947cb82e4 100644 --- a/src/subagent/idempotent-task-exempt.test.ts +++ b/src/subagent/idempotent-task-exempt.test.ts @@ -12,9 +12,9 @@ function taskResult(): ToolResult { describe("isIdempotentTaskBatch", () => { test("manage_tasks batch is exempt", () => { - expect( - isIdempotentTaskBatch([call("manage_tasks")], [taskResult()]), - ).toBe(true); + expect(isIdempotentTaskBatch([call("manage_tasks")], [taskResult()])).toBe( + true, + ); }); test("todowrite alias is exempt", () => { @@ -48,12 +48,12 @@ describe("isIdempotentTaskBatch", () => { }); test("non-task tools are never exempt", () => { - expect( - isIdempotentTaskBatch([call("run_shell")], [taskResult()]), - ).toBe(false); + expect(isIdempotentTaskBatch([call("run_shell")], [taskResult()])).toBe( + false, + ); }); test("empty batches are never exempt", () => { expect(isIdempotentTaskBatch([], [])).toBe(false); }); -}); \ No newline at end of file +}); diff --git a/src/subagent/idempotent-task-exempt.ts b/src/subagent/idempotent-task-exempt.ts index 2038052ce..3a2d7ba1d 100644 --- a/src/subagent/idempotent-task-exempt.ts +++ b/src/subagent/idempotent-task-exempt.ts @@ -20,4 +20,4 @@ export function isIdempotentTaskBatch( ): boolean { if (calls.length === 0) return false; return calls.every((call) => isSameTool(call.name, "manage_tasks")); -} \ No newline at end of file +} From c73e35aab79dfb366d36afadb855b15d20edc074 Mon Sep 17 00:00:00 2001 From: "user.email" Date: Thu, 8 Oct 2026 22:12:49 -0700 Subject: [PATCH 3/3] chore(ci): ignore benchmark and working-note scratch dirs plans/, tb-*/** and terminal-bench/ are local-only working notes and benchmark scratch, never committed. They leaked into CI's 'bunx oxfmt --check .' (static-analysis) scan, failing every PR that picks them up from the environment. Ignore them so oxfmt scans only trackable source. --- .gitignore | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitignore b/.gitignore index 8187eebb8..046e458b7 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,14 @@ subagents/ # Working notes and design spikes — local only, never committed docs/plans/ +# Fleet/harness working notes — local only, never committed +plans/ + +# Benchmark/eval scratch — local only, never committed +tb-dataset/ +tb-rerun-50/ +terminal-bench/ +evals/**/__pycache__/ # Scratch repro tests — never committed __repro/