From 55c4efec4e3198770b42a6150e6679f40bffca6c Mon Sep 17 00:00:00 2001 From: QianYan-Art Date: Sat, 6 Jun 2026 04:05:04 -0700 Subject: [PATCH] fix(coding-agent): honor ttsr.enabled: false in TtsrManager `TtsrManager` previously ignored the `ttsr.enabled: false` toggle: - `addRule()` still registered rules, so `bucketRules` demoted them to the TTSR bucket and dropped the rulebook/alwaysApply fallback - `hasRules()` still returned true, so `AgentSession` entered the TTSR matching path on every message_update - `checkDelta` / `checkSnapshot` (via `#matchBuffer`) still matched patterns and could abort the stream and inject system reminders Gate all three public-ish entry points on `#settings.enabled` so disabling TTSR short-circuits the whole path. Condition rules fall through to the rulebook bucket in `bucketRules` instead of being silently swallowed. Closes #1767 --- packages/coding-agent/CHANGELOG.md | 4 ++ packages/coding-agent/src/export/ttsr.ts | 9 +++ .../test/capability/rule-buckets.test.ts | 19 ++++++ packages/coding-agent/test/ttsr.test.ts | 58 +++++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e14f2353f..ec4cd0831 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `ttsr.enabled: false` being ignored at runtime. TTSR rules were still being registered with `TtsrManager.addRule` and matched against stream deltas even when the global toggle was off, so disabling TTSR did not suppress rule injection or stream abort. The manager now gates `addRule`, `hasRules`, and `#matchBuffer` on the enabled flag, so disabling fully short-circuits the TTSR path. Condition rules fall through to the rulebook bucket instead of being silently swallowed. ([#1767](https://github.com/can1357/oh-my-pi/issues/1767)) + ## [15.9.5] - 2026-06-05 ### Added diff --git a/packages/coding-agent/src/export/ttsr.ts b/packages/coding-agent/src/export/ttsr.ts index fa973b420..8dba7a399 100644 --- a/packages/coding-agent/src/export/ttsr.ts +++ b/packages/coding-agent/src/export/ttsr.ts @@ -294,6 +294,9 @@ export class TtsrManager { /** Add a TTSR rule to be monitored. */ addRule(rule: Rule): boolean { + if (!this.#settings.enabled) { + return false; + } if (this.#rules.has(rule.name)) { return false; } @@ -357,6 +360,9 @@ export class TtsrManager { } #matchBuffer(buffer: string, context: TtsrMatchContext): Rule[] { + if (!this.#settings.enabled) { + return []; + } const matches: Rule[] = []; for (const [name, entry] of this.#rules) { if (!this.#canTrigger(name)) { @@ -433,6 +439,9 @@ export class TtsrManager { /** Check if any TTSR rules are registered. */ hasRules(): boolean { + if (!this.#settings.enabled) { + return false; + } return this.#rules.size > 0; } diff --git a/packages/coding-agent/test/capability/rule-buckets.test.ts b/packages/coding-agent/test/capability/rule-buckets.test.ts index 96bbfa273..c2b1a6a6c 100644 --- a/packages/coding-agent/test/capability/rule-buckets.test.ts +++ b/packages/coding-agent/test/capability/rule-buckets.test.ts @@ -96,4 +96,23 @@ describe("bucketRules", () => { expect(mgr.checkDelta("contains FORBIDDEN token", { source: "text" }).map(r => r.name)).toEqual(["builtin-foo"]); }); + + it("falls condition rules through to the rulebook when ttsr is disabled on the manager", () => { + const mgr = new TtsrManager({ + enabled: false, + contextMode: "discard", + interruptMode: "always", + repeatMode: "once", + repeatGap: 10, + }); + const ttsr = makeRule({ name: "no-foo", condition: ["FORBIDDEN"], description: "blocks foo" }); + + const { rulebookRules, alwaysApplyRules } = bucketRules([ttsr], mgr); + + // Manager refused to register; condition rule degrades to its rulebook shape. + expect(mgr.hasRules()).toBe(false); + expect(mgr.checkDelta("contains FORBIDDEN token", { source: "text" })).toEqual([]); + expect(alwaysApplyRules.map(r => r.name)).toEqual([]); + expect(rulebookRules.map(r => r.name)).toEqual(["no-foo"]); + }); }); diff --git a/packages/coding-agent/test/ttsr.test.ts b/packages/coding-agent/test/ttsr.test.ts index 2053cdc9c..84579de08 100644 --- a/packages/coding-agent/test/ttsr.test.ts +++ b/packages/coding-agent/test/ttsr.test.ts @@ -1,9 +1,21 @@ import { describe, expect, it } from "bun:test"; import * as path from "node:path"; import { parseRuleConditionAndScope, type Rule } from "@oh-my-pi/pi-coding-agent/capability/rule"; +import type { TtsrSettings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { EDIT_MODE_STRATEGIES } from "@oh-my-pi/pi-coding-agent/edit"; import { TtsrManager } from "@oh-my-pi/pi-coding-agent/export/ttsr"; +function ttsrManager(overrides: Partial = {}): TtsrManager { + return new TtsrManager({ + enabled: true, + contextMode: "discard", + interruptMode: "always", + repeatMode: "once", + repeatGap: 10, + ...overrides, + }); +} + function makeRule(partial: Partial): Rule { return { name: partial.name ?? "rule", @@ -274,6 +286,52 @@ describe("TtsrManager scope matching", () => { }); }); +describe("TtsrManager enabled gate", () => { + it("rejects registration when ttsr is disabled", () => { + const manager = ttsrManager({ enabled: false }); + const rule = makeRule({ + name: "no-foo", + condition: ["FORBIDDEN"], + scope: ["text"], + }); + + expect(manager.addRule(rule)).toBe(false); + }); + + it("reports no rules when ttsr is disabled, even after a registration attempt", () => { + const manager = ttsrManager({ enabled: false }); + manager.addRule( + makeRule({ + name: "no-foo", + condition: ["FORBIDDEN"], + scope: ["text"], + }), + ); + + expect(manager.hasRules()).toBe(false); + }); + + it("returns no matches from stream deltas when ttsr is disabled", () => { + const manager = ttsrManager({ enabled: false }); + + expect(manager.checkDelta("contains FORBIDDEN token", { source: "text" })).toEqual([]); + expect(manager.checkDelta("FORBIDDEN", { source: "tool", toolName: "edit" })).toEqual([]); + }); + + it("preserves the default (enabled) registration and matching contract", () => { + const manager = ttsrManager(); + const rule = makeRule({ + name: "no-foo", + condition: ["FORBIDDEN"], + scope: ["text"], + }); + + expect(manager.addRule(rule)).toBe(true); + expect(manager.hasRules()).toBe(true); + expect(manager.checkDelta("FORBIDDEN", { source: "text" })).toEqual([rule]); + }); +}); + describe("TtsrManager snapshot matching", () => { it("matches source-level conditions against a tool digest where the raw patch grammar fails", () => { const manager = new TtsrManager();