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 @@ -35,6 +35,15 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
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.
- Removed degenerate-repetition detection outright: the streamed-text loop
detector, the tool-fingerprint period/cycle thrash check, the
turns-since-user-message backstop, and the leaf no-progress (identical
tool-call) counter. These were pattern-matching heuristics layered on top
of the transport/policy line the harness actually needs — provider stream
error handling, connection retry/backoff, and the turn budget — and had
become a source of false-positive stalls without a clear win rate. The
turn budget stop and the director's soft tool-only check-in nudge are
unchanged; nothing else in this run/stop chain was touched.

## [0.2.108] - 2026-08-24

Expand Down
968 changes: 1 addition & 967 deletions src/agent/director.test.ts

Large diffs are not rendered by default.

237 changes: 14 additions & 223 deletions src/agent/director.ts

Large diffs are not rendered by default.

6 changes: 2 additions & 4 deletions src/agent/model-family-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ export interface ModelFamilyPolicy {
* Consecutive tool-only assistant turns (tool calls, no text) before the
* main chat director injects a one-shot wrap-up nudge. A long tool-only
* streak is normal orchestration (Linear lookups, code reads, etc.) and
* must not by itself stop the session — this is a soft check-in, not a
* loop-protection trigger. The real stop signal is a repeating cycle in
* the tool-fingerprint history, independent of this threshold — see
* detectToolFingerprintThrash in subagent/stop-policy.ts.
* must not by itself stop the session — this is a soft check-in, and it
* never escalates to a pause on its own.
*/
toolOnlyTurnNudgeAt: number;
/** Ephemeral nudge text injected at toolOnlyTurnNudgeAt. */
Expand Down
2 changes: 1 addition & 1 deletion src/agent/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export function buildGuidelines(
"- Prefer the typed spawn contract on every worker: `intent`, `success_criteria` (done-when), `do_not` (scope fence), and `report_focus` so workers finish instead of thrashing. Free-form `prompt` alone is weaker.",
"- After workers return, merge their Summary/Findings into a coherent answer for the operator; do not paste raw sub-agent dumps.",
"- Pass `maxTurns` on `task` when a job needs a bounded inference budget (unset is unbounded). On turn-budget salvage, re-dispatch with continuation context and a higher maxTurns only a few times on the same brief — after the re-dispatch cap, change approach instead of bumping turns again.",
"- After thrash / no-progress / repetition / never-acted salvage, do not re-dispatch an identical brief (prompt/agent/intent/success_criteria/do_not) — it is refused. Change the brief to force a re-run; maxTurns alone does not unlock it.",
"- After a thrash / no-ship / never-acted / never-edited salvage, do not re-dispatch an identical brief (prompt/agent/intent/success_criteria/do_not) — it is refused. Change the brief to force a re-run; maxTurns alone does not unlock it.",
"- Use manage_tasks for your own coordination checklist; spawning workers is `task`, not manage_tasks.",
"- If context is compacted automatically, do not stop tasks early due to token fear; persist progress via manage_tasks and worker reports.",
]),
Expand Down
10 changes: 5 additions & 5 deletions src/session/stream-journal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ describe("createCycleTextRecorder", () => {
test("flush writes the buffer with a reason and resets", async () => {
const recorder = createCycleTextRecorder(() => dir);
recorder.handleEvent(delta("looping output"));
await recorder.flush("repetition");
await recorder.flush("cancelled");

const records = await readPartialRecords();
expect(records).toHaveLength(1);
expect(records[0]?.reason).toBe("repetition");
expect(records[0]?.reason).toBe("cancelled");
expect(records[0]?.text).toBe("looping output");
expect(recorder.text()).toBe("");
});
Expand Down Expand Up @@ -177,7 +177,7 @@ describe("createCycleTextRecorder", () => {
expect(recorder.text()).toBe("visible reply");
expect(recorder.thinkingText()).toBe("0/1 1/2 2/3 ");

await recorder.flush("repetition");
await recorder.flush("cancelled");
const records = await readPartialRecords();
expect(records[0]?.text).toBe("visible reply");
expect(records[0]?.thinkingText).toBe("0/1 1/2 2/3 ");
Expand All @@ -189,11 +189,11 @@ describe("createCycleTextRecorder", () => {
// must still be diagnosable from thinkingText alone.
const recorder = createCycleTextRecorder(() => dir);
recorder.handleEvent(thinkingDelta("0/1 1/2 2/3 3/4 4/5 "));
const snapshot = await recorder.dispose("repetition");
const snapshot = await recorder.dispose("cancelled");

expect(snapshot).toBe("");
const records = await readPartialRecords();
expect(records[0]?.reason).toBe("repetition");
expect(records[0]?.reason).toBe("cancelled");
expect(records[0]?.text).toBe("");
expect(records[0]?.thinkingText).toBe("0/1 1/2 2/3 3/4 4/5 ");
});
Expand Down
1 change: 0 additions & 1 deletion src/session/stream-journal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export function appendCycleText(
}

export type PartialFlushReason =
| "repetition"
| "deadline"
| "cancelled"
| "interrupted"
Expand Down
9 changes: 1 addition & 8 deletions src/session/summarizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { createDefaultDependencies } from "@intx/inference/providers";
import { getLogger } from "@intx/log";
import type { ConversationTurn, InferenceSource } from "@intx/types/runtime";
import { LOG_NAMESPACE_ROOT } from "../branding.js";
import { detectRepetition } from "../subagent/repetition.js";
import { buildTurnSummary } from "./compactor.js";

const logger = getLogger([LOG_NAMESPACE_ROOT, "session", "summarizer"]);
Expand Down Expand Up @@ -75,13 +74,7 @@ export function condenseTurns(turns: ConversationTurn[]): string {
if (turn.role === "user") {
userMessages.push(block.text.slice(0, 400));
} else if (turn.role === "assistant" && block.text.length > 0) {
// Compaction often fires mid-degeneration, when the tail of the
// history is the model looping one phrase. Seeding the summary from
// those turns hands the looped text to the summarizer verbatim, so
// repetition-flagged turns are dropped from the excerpt entirely.
if (detectRepetition(block.text) === null) {
assistantSnippets.push(block.text.slice(0, 300));
}
assistantSnippets.push(block.text.slice(0, 300));
}
}
if (block.type === "tool_call") {
Expand Down
11 changes: 2 additions & 9 deletions src/subagent/brief-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import type { TaskIntent } from "./report.js";
import type { ForcedStopReason } from "./stop-policy.js";

/** Salvage classes that must not be re-dispatched with an identical brief. */
export type HardBlockSalvage =
"no-ship" | "no-progress" | "repetition" | "never-acted" | "never-edited";
export type HardBlockSalvage = "no-ship" | "never-acted" | "never-edited";

// Every forced-stop reason a leaf can report maps 1:1 onto a salvage kind
// the parent ledger cares about.
Expand Down Expand Up @@ -44,13 +43,7 @@ export interface BriefDispatchRecord {
*/
export const TURN_BUDGET_STOP_AFTER_DISPATCHES = 3;

const HARD_BLOCK_SALVAGES = new Set<BriefSalvageKind>([
"no-ship",
"no-progress",
"repetition",
"never-acted",
"never-edited",
]);
const HARD_BLOCK_SALVAGES = new Set<BriefSalvageKind>(["no-ship", "never-acted", "never-edited"]);

export function isHardBlockSalvage(kind: BriefSalvageKind): kind is HardBlockSalvage {
return HARD_BLOCK_SALVAGES.has(kind);
Expand Down
Loading
Loading