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
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<TtsrSettings> = {}): TtsrManager {
|
||||
return new TtsrManager({
|
||||
enabled: true,
|
||||
contextMode: "discard",
|
||||
interruptMode: "always",
|
||||
repeatMode: "once",
|
||||
repeatGap: 10,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function makeRule(partial: Partial<Rule>): 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();
|
||||
|
||||
Reference in New Issue
Block a user