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

> Discover how Time-Traveling Stream Rules (TTSR) in oh-my-pi inject corrections during code generation by monitoring output, aborting, and retrying with system messages.

- Repository: [Can Bölük/oh-my-pi](https://github.com/can1357/oh-my-pi)
- Tags: deep-dive
- Published: 2026-05-21

---

**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`](https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/export/ttsr.ts) looks like this:

```ts
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.

```ts
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`](https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/export/ttsr.ts):

```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:

```ts
#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`](https://github.com/can1357/oh-my-pi/blob/main/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`](https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/prompts/system/ttsr-interrupt.md) and appended as a hidden `custom_message` with `customType: "ttsr-injection"`:

```xml
<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`](https://github.com/can1357/oh-my-pi/blob/main/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`](https://github.com/can1357/oh-my-pi/blob/main/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:

```json
{
  "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:

```xml
<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`](https://github.com/can1357/oh-my-pi/blob/main/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`](https://github.com/can1357/oh-my-pi/blob/main/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`](https://github.com/can1357/oh-my-pi/blob/main/ttsr-interrupt.md) to build `<system-interrupt>` XML blocks, while non-interrupting tool rules use [`ttsr-tool-reminder.md`](https://github.com/can1357/oh-my-pi/blob/main/ttsr-tool-reminder.md) to create `<system-reminder>` blocks that guide the assistant's subsequent output.