How Time-Traveling Stream Rules (TTSR) Inject Corrections During Code Generation in oh-my-pi

Time-Traveling Stream Rules (TTSR) inject corrections by monitoring the assistant's streaming output in real-time, aborting the generation when a regex condition matches, and scheduling a retry with a corrective system message appended to the conversation.

In the oh-my-pi coding agent, TTSR is a runtime mechanism that watches text, thinking blocks, and tool-call arguments as they stream from the LLM. When a rule's regular-expression condition matches the buffered content, the system can interrupt the current turn mid-stream, discard or keep the partial output, and inject a corrective prompt before restarting generation.

Registration and Monitoring Setup

When a session initializes via createAgentSession(), discovered rules are loaded and a TtsrManager instance is created from the ttsr settings group. Each rule undergoes compilation (#compileConditions) and scope validation (#buildScope) before being stored in the manager's #rules array.

Only rules with at least one reachable scope are retained. The registration flow in packages/coding-agent/src/export/ttsr.ts looks like this:

const ttsrSettings = settings.getGroup("ttsr");
const ttsrManager = new TtsrManager(ttsrSettings);
await loadCapability<Rule>(ruleCapability.id, { cwd });
for (const rule of rulesResult.items) {
  if (rule.condition?.length && ttsrManager.addRule(rule)) continue;
}

Stream Buffering and Delta Processing

During active generation, every delta emitted by the assistant (message_update events) is passed to TtsrManager.checkDelta(delta, context). The manager selects a buffer key based on the source type—text, thinking, or a specific tool call ID—using the private #bufferKey method, then appends the content to an internal Map.

const bufferKey = this.#bufferKey(context);
const nextBuffer = `${this.#buffers.get(bufferKey) ?? ""}${delta}`;
this.#buffers.set(bufferKey, nextBuffer);

This buffering allows TTSR to evaluate conditions against the cumulative content of the stream, not just individual chunks.

Rule Matching Logic

For each stored rule, the manager performs a sequence of checks in order:

  1. Repeat gating (#canTrigger) enforces once or after-gap policies to prevent duplicate triggers
  2. Scope validation (#matchesScope) verifies allowText, allowThinking, or specific tool scopes
  3. Global path globs (#matchesGlobalPaths) applies optional file-path filters
  4. Condition regex (#matchesCondition) matches the compiled pattern against the entire buffered chunk

The matching logic in packages/coding-agent/src/export/ttsr.ts:

if (!this.#canTrigger(name)) continue;
if (!this.#matchesScope(entry, context)) continue;
if (!this.#matchesGlobalPaths(entry, context)) continue;
if (!this.#matchesCondition(entry, nextBuffer)) continue;
matches.push(entry.rule);

If all checks pass, the rule is queued for injection.

Interrupt vs. Non-Interrupt Injection

TTSR supports two injection strategies depending on the rule's interruptMode and source:

Interrupting matches occur when interruptMode is "always" and the match allows interruption. These rules are placed in #pendingTtsrInjections. The session immediately aborts the stream via agent.abort(), emits a ttsr_triggered event, and schedules a retry after 50 ms:

#ttsrAbortPending = true;
agent.abort();               // stop the current stream
emitEvent({type:"ttsr_triggered",rules}); // async notification
setTimeout(() => retry(), 50);

Non-interrupting matches occur when source === "tool" or interruptMode is "never". These are stored in #perToolTtsrInjections. The stream continues uninterrupted, and when the tool completes, a <system-reminder> block is prepended to the tool's output via the afterToolCall hook.

Retry and Correction Flow

When the retry timeout fires in packages/coding-agent/src/session/agent-session.ts, the manager clears #ttsrAbortPending and reads the contextMode (discard or keep):

  • discard: The partial assistant message that triggered the rule is removed via agent.replaceMessages()
  • keep: The partial output remains in the conversation history

A system-interrupt message is then built from the template in packages/coding-agent/src/prompts/system/ttsr-interrupt.md and appended as a hidden custom_message with customType: "ttsr-injection":

<system-interrupt reason="rule_violation" rule="{{name}}" path="{{path}}">
  …generated correction…
</system-interrupt>

The rule names are persisted via sessionManager.appendTtsrInjection(), and agent.continue() restarts generation with the correction visible in the context.

Persistence and Session State

Injected rule names are stored as ttsr_injection entries in the session history to prevent re-triggering on resume. During session restoration, createAgentSession() calls ttsrManager.restoreInjected(existingSession.injectedTtsrRules) to re-hydrate these records from packages/coding-agent/src/session/session-manager.ts.

The AgentSessionEvent of type "ttsr_triggered" carries the matched Rule[] array, allowing extensions to listen via on("ttsr_triggered", …) or receive notifications in onSession({reason:"ttsr_triggered", rules}). The UI component in packages/coding-agent/src/modes/components/ttsr-notification.ts renders these events in interactive mode.

Defining a TTSR Rule Configuration

Rules are defined as JSON objects with regex conditions, scopes, and repetition policies. This example triggers when the assistant attempts to use var declarations in TypeScript files:

{
  "name": "avoid‑global‑var",
  "condition": ["\\bvar\\s+\\w+\\b"],
  "scope": ["text"],
  "globs": ["src/**/*.ts"],
  "repeatMode": "once"
}

When matched, the injection appears in the transcript as:

<system-interrupt reason="rule_violation" rule="avoid‑global‑var" path="src/utils/helpers.ts">
  Please avoid using `var`; prefer `let` or `const` for block‑scoped variables.
</system-interrupt>

The assistant then continues generation after the reminder, respecting the rule's repeat policy.

Summary

  • Real-time monitoring: TtsrManager.checkDelta() buffers streaming content from text, thinking, or tool sources to enable mid-stream pattern detection.
  • Multi-layer matching: Rules are validated through repeat gating, scope checks, path globbing, and regex conditions before triggering.
  • Abort and retry: Interrupting rules call agent.abort() and schedule a 50 ms retry with a system-interrupt message injected from ttsr-interrupt.md.
  • State persistence: Injected rules are tracked in ttsr_injection entries and restored via restoreInjected() to prevent duplicate corrections across session resumes.
  • Flexible scope: Tool-sourced rules can be non-interrupting, with corrections injected via afterToolCall hooks using ttsr-tool-reminder.md.

Frequently Asked Questions

How does TTSR prevent triggering the same rule repeatedly?

The #canTrigger method enforces repeatMode policies including once (never re-trigger) and after-gap (trigger only after other content has passed). Triggered rule names are also persisted in ttsr_injection history entries, which are restored on session resume via restoreInjected() to maintain state across interruptions.

What is the difference between discard and keep context modes?

When contextMode is set to discard, the partial assistant message that triggered the TTSR rule is removed from the conversation via agent.replaceMessages() before the retry. When set to keep, the partial output remains visible to the model, preserving the context of the original generation attempt alongside the corrective injection.

Can TTSR rules target specific tool calls rather than text output?

Yes. The #bufferKey method generates distinct keys for tool-specific buffers, and the #matchesScope method validates tool-scoped rules. Tool-sourced rules typically use non-interrupting injection, storing corrections in #perToolTtsrInjections and prepending reminders via the afterToolCall hook rather than aborting the stream.

Where does the actual correction text come from?

The correction payload is generated from template files in the packages/coding-agent/src/prompts/system/ directory. Interrupting rules use ttsr-interrupt.md to build <system-interrupt> XML blocks, while non-interrupting tool rules use ttsr-tool-reminder.md to create <system-reminder> blocks that guide the assistant's subsequent output.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →