Skip to content

feat(ttsr): Port Time Traveling Streamed Rules from oh-my-pi#2275

Draft
minpeter wants to merge 11 commits intocode-yeongyu:devfrom
minpeter:feat/ttsr
Draft

feat(ttsr): Port Time Traveling Streamed Rules from oh-my-pi#2275
minpeter wants to merge 11 commits intocode-yeongyu:devfrom
minpeter:feat/ttsr

Conversation

@minpeter
Copy link
Collaborator

@minpeter minpeter commented Mar 3, 2026

Summary

Port the full TTSR (Time Traveling Streamed Rules) system from can1357/oh-my-pi to oh-my-opencode. TTSR monitors LLM output streams in real-time for regex patterns, aborts the stream on match, injects a <system-interrupt> corrective prompt, and retries automatically.

Primary use case: gpt-5.3-codex outputs to=multi_tool_use.parallel instead of proper tool calls — TTSR detects this, stops, injects a reminder, and retries.

What's Included

New Modules (src/features/ttsr/)

  • types.ts — 6 core types: TtsrMatchSource, TtsrMatchContext, TtsrScope, TtsrEntry, TtsrSettings, TtsrRule
  • ttsr-manager.ts — Pure state machine (176 LOC): buffer management, regex matching, repeatMode ("once"/"after-gap"), markInjected() tracking
  • scope-parser.ts — Parses scope strings (text, tool:edit, tool:edit(*.rs)) with picomatch glob support
  • rule-parser.ts — Parses TTSR rule files (YAML frontmatter + body) using existing parseFrontmatter()
  • interrupt-template.ts — Renders <system-interrupt reason="rule_violation"> XML templates
  • index.ts — Barrel export

New Hooks (src/hooks/ttsr/)

  • hook.tscreateTtsrHook() factory: listens to session.created/deleted, message.updated, message.part.updated; delegates to TtsrManager for stream monitoring
  • abort-retry-handler.ts — Abort+retry orchestrator with maxRetriesPerRule enforcement and retryCounts tracking
  • rule-discovery.ts — Discovers .ttsr rule files from .claude/rules/, .opencode/rules/, ~/.claude/rules/
  • ttsr-integration.test.ts — End-to-end integration test

Plugin Wiring (minimal, scoped changes)

  • src/config/schema/ttsr.ts — Zod v4 config schema (6 fields: enabled, contextMode, interruptMode, repeatMode, repeatGap, maxRetriesPerRule)
  • src/config/schema/oh-my-opencode-config.ts — Added ttsr optional field
  • src/config/schema/hooks.ts — Added "ttsr" to HookNameSchema
  • src/hooks/index.ts — Barrel export for createTtsrHook + TtsrHook
  • src/plugin/hooks/create-session-hooks.ts — TTSR hook registration with abort/prompt wiring
  • src/plugin/event.ts — 1-line dispatch addition

Test Coverage

  • 73 TTSR-specific tests, 0 failures
  • 78 total including config schema + integration tests
  • TDD (red-green-refactor) for all modules
  • Edge cases covered: empty rules, malformed scopes, missing frontmatter, maxRetriesPerRule exhaustion, repeatMode "once" + "after-gap"

Bug Fixes (discovered during QA)

  1. markInjected() never called — Fixed via hookInstance mutable reference pattern. Without this, repeatMode: "once" caused infinite retry loops
  2. Async rule discovery race condition — Added addRulesToExistingManagers() to inject discovered rules into already-initialized session managers
  3. maxRetriesPerRule not enforced — Added retryCounts Map with per-session, per-rule tracking
  4. Wrong fallback valuesinterruptMode fallback corrected to "always", repeatGap to 10 (matching Zod defaults)
  5. Silent error swallowing — Replaced 3 empty .catch(() => {}) with proper log() calls

Stats

24 files changed, 2129 insertions(+), 2 deletions(-)
  • All production files under 200 LOC limit
  • Zero as any / @ts-ignore / @ts-expect-error
  • Zero empty catch blocks
  • Uses existing conventions: picomatch, parseFrontmatter(), log(), safeHook(), factory pattern
  • No new npm dependencies

Configuration

// .opencode/oh-my-opencode.jsonc
{
  "ttsr": {
    "enabled": true,
    "contextMode": "discard",     // "discard" | "keep"
    "interruptMode": "always",    // "always" | "prose-only" | "tool-only" | "never"
    "repeatMode": "once",         // "once" | "after-gap"
    "repeatGap": 10,              // messages gap for "after-gap" mode
    "maxRetriesPerRule": 3        // max retries per rule per session
  }
}

Commits

  • da3eb1a6 feat(ttsr): add TTSR types, config schema, and interrupt template
  • a8830233 feat(ttsr): implement TtsrManager, scope parser, and rule parser with TDD
  • 2ec5edc1 feat(ttsr): implement abort-retry handler, rule discovery, hook tests, and barrel export
  • a912eea7 feat(ttsr): register TTSR hook into plugin system
  • 525069fa test(ttsr): add end-to-end integration test
  • 43c33e98 fix(ttsr): add missing hook.ts
  • e5429f46 fix(ttsr): resolve three critical bugs
  • 02a9939b fix(ttsr): correct wiring fallback values and add error logging

Summary by cubic

Adds Time Traveling Streamed Rules (TTSR) to monitor assistant output in real time, abort on rule violations, inject a system-interrupt prompt, and retry. TTSR is disabled by default (opt-in) and ships a built-in guard that blocks codex-style plain text tool syntax like "multi_tool_use.parallel".

  • New Features

    • Real-time stream monitoring with isolated buffers and scoped matching for text, thinking, and tools (with file globs).
    • Markdown rule parsing (YAML frontmatter) with legacy trigger fields supported; renders prompts.
    • Built-in rule: "multi_tool_use.parallel" guard loaded when TTSR is enabled; discovered rules from .claude/rules, .sisyphus/rules, and ~/.claude/rules append and can override by name.
    • Abort + retry handler with per-session, per-rule limits (maxRetriesPerRule).
    • Plugin integration: config schema, hook registration, event dispatch; defaults: enabled=false (opt-in), contextMode=discard, interruptMode=always, repeatMode=once, repeatGap=10, maxRetriesPerRule=3.
    • Config shorthand: "ttsr": true/false expands to full config with defaults.
  • Bug Fixes

    • Call markInjected to prevent repeatMode=once from looping.
    • Inject discovered rules into existing managers to fix discovery race.
    • Enforce maxRetriesPerRule via per-session tracking.
    • Correct interruptMode and repeatGap fallbacks to match schema.
    • Replace silent catches with log() calls for abort, prompt, and discovery.

Written for commit ad113c2. Summary will update on new commits.

minpeter added 11 commits March 3, 2026 22:37
…e discovery race, maxRetriesPerRule enforcement

- Call markInjected() via hookInstance mutable reference in onMatch callback
  to fix repeatMode 'once' infinite retry loops
- Add addRulesToExistingManagers() to TtsrHook interface and implementation
  to inject discovered rules into already-initialized session managers
- Enforce maxRetriesPerRule with retryCounts Map in abort-retry-handler,
  filtering eligible rules before triggering abort-retry cycle
- Add clearRetryCounts() for session cleanup
- Add tests for all three bug fixes (73 pass, 0 fail)
…tch blocks

- Fix interruptMode fallback: 'abort-retry' → 'always' (matches Zod schema default)
- Fix repeatGap fallback: 3 → 10 (matches Zod schema default)
- Replace 3 empty .catch(() => {}) with proper log() calls for abort, prompt, and rule discovery
- Update schema.json (regenerated by build)
Ship a default TTSR rule that detects GPT models outputting
'multi_tool_use.parallel' (not a real tool in this environment)
and triggers abort-retry with a corrective system interrupt.

- Add createBuiltinTtsrRules() factory in builtin-rules.ts
- Pre-populate TTSR rules array with built-in defaults
- User-discovered rules from .claude/rules/ etc. append on top
- Built-in rule can be overridden by same-name user rule
Allow `"ttsr": true` as shorthand for `"ttsr": { "enabled": true }`.
Boolean values are transformed to full config objects with all defaults.
Existing object-form config continues to work unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant