Files
oh-my-pi/docs/ttsr-injection-lifecycle.md
T
2026-04-30 06:47:01 +02:00

8.2 KiB

TTSR Injection Lifecycle

This document covers the current Time Traveling Stream Rules (TTSR) runtime path from rule discovery to stream interruption, retry injection, extension notifications, and session-state handling.

Implementation files

1. Discovery feed and rule registration

At session creation, createAgentSession() loads discovered rules and constructs a TtsrManager:

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

Pre-registration dedupe behavior

loadCapability("rules") deduplicates by rule.name with first-wins semantics (higher provider priority first). Shadowed duplicates are removed before TTSR registration.

TtsrManager.addRule() behavior

Registration is skipped when:

  • rule.condition is absent or all condition regexes fail to compile
  • a rule with the same rule.name was already registered in this manager
  • the rule scope excludes all monitored streams

Invalid regex conditions and unreachable scopes are logged as warnings and ignored; session startup continues.

Setting caveat

TtsrSettings.enabled is loaded into the manager but is not currently checked in runtime gating. If TTSR rules exist, matching still runs.

2. Streaming monitor lifecycle

TTSR detection runs inside AgentSession.#handleAgentEvent.

Turn start

On turn_start, the stream buffer is reset:

  • ttsrManager.resetBuffer()

During stream (message_update)

When assistant updates arrive and rules exist:

  • monitor text_delta, thinking_delta, and toolcall_delta
  • append delta into a source/tool scoped manager buffer
  • call checkDelta(delta, matchContext)

checkDelta() iterates registered rules and returns all matching rules that pass scope, global-path, condition, and repeat policy checks.

3. Trigger decision and immediate abort path

When one or more rules match and at least one matched rule allows interruption:

  1. Matched rules are deduplicated into #pendingTtsrInjections.
  2. #ttsrAbortPending = true and a TTSR resume gate is created.
  3. agent.abort() is called immediately.
  4. ttsr_triggered event is emitted asynchronously (fire-and-forget).
  5. retry work is scheduled via the post-prompt task scheduler with a 50ms delay.

Abort is not blocked on extension callbacks.

4. Retry scheduling, context mode, and reminder injection

After the 50ms timeout:

  1. #ttsrAbortPending = false
  2. read ttsrManager.getSettings().contextMode
  3. if contextMode === "discard", drop the targeted partial assistant output with agent.replaceMessages(...slice(0, targetAssistantIndex))
  4. build injection content from pending rules using ttsr-interrupt.md template
  5. append and persist a hidden custom_message/runtime custom message with customType: "ttsr-injection" and details.rules
  6. mark those rule names injected, persist a ttsr_injection entry, and call agent.continue() to retry generation

Template payload is:

<system-interrupt reason="rule_violation" rule="{{name}}" path="{{path}}">
...
{{content}}
</system-interrupt>

Pending injections are cleared after content generation.

contextMode behavior on partial output

  • discard: partial/aborted assistant message is removed before retry.
  • keep: partial assistant output remains in conversation state; reminder is appended after it.

Non-interrupting matches

If matched rules do not permit interruption (interruptMode: "never", or source-specific prose-only/tool-only mismatch), they are still queued. After a successful non-error, non-aborted assistant message, AgentSession injects the hidden ttsr-injection custom message as a follow-up and schedules continuation.

5. Repeat policy and gap logic

TtsrManager tracks #messageCount and per-rule lastInjectedAt.

repeatMode: "once"

A rule can trigger only once after it has an injection record.

repeatMode: "after-gap"

A rule can re-trigger only when:

  • messageCount - lastInjectedAt >= repeatGap

messageCount increments on turn_end, so gap is measured in completed turns, not stream chunks.

6. Event emission and extension/hook surfaces

Session event

AgentSessionEvent includes:

{ type: "ttsr_triggered"; rules: Rule[] }

Extension runner

#emitSessionEvent() routes the event to:

  • extension listeners (ExtensionRunner.emit({ type: "ttsr_triggered", rules }))
  • local session subscribers

Hook and custom-tool typing

  • extension API exposes on("ttsr_triggered", ...)
  • hook API exposes on("ttsr_triggered", ...)
  • custom tools receive onSession({ reason: "ttsr_triggered", rules })

Interactive-mode rendering difference

Interactive mode uses session.isTtsrAbortPending to suppress showing the aborted assistant stop reason as a visible failure during TTSR interruption, and renders a TtsrNotificationComponent when the event arrives.

7. Persistence and resume state (current implementation)

SessionManager persists injected-rule state:

  • entry type: ttsr_injection
  • append API: appendTtsrInjection(ruleNames)
  • query API: getInjectedTtsrRules()
  • context reconstruction includes SessionContext.injectedTtsrRules

TtsrManager supports restoration via restoreInjected(ruleNames).

Current wiring status

In the current runtime path:

  • interrupted injections append a hidden custom_message with customType: "ttsr-injection" and append a ttsr_injection entry via appendTtsrInjection(...)
  • deferred non-interrupting injections are marked/persisted when their queued custom message reaches message_end
  • createAgentSession() restores existingSession.injectedTtsrRules into ttsrManager

Net effect: injected-rule suppression is persisted/restored across session reload/resume for the current branch path.

8. Race boundaries and ordering guarantees

Abort vs retry callback

  • abort is synchronous from TTSR handler perspective (agent.abort() called immediately)
  • retry is deferred by timer (50ms)
  • extension notification is asynchronous and intentionally not awaited before abort/retry scheduling

Multiple matches in same stream window

checkDelta() returns all currently matching eligible rules for that scoped buffer. Pending injections are deduplicated by rule name before injection.

Between abort and continue

During the timer window, state can change (user interruption, mode actions, additional events). The retry call is best-effort: agent.continue().catch(() => {}) swallows follow-up errors.

9. Edge cases summary

  • Invalid condition regex: skipped with warning; other conditions/rules continue.
  • Duplicate rule names at capability layer: lower-priority duplicates are shadowed before registration.
  • Duplicate names at manager layer: second registration is ignored.
  • contextMode: "keep": partial violating output can remain in context before reminder retry.
  • interruptMode: "never" queues a deferred hidden injection after a successful assistant message rather than aborting mid-stream.
  • Repeat-after-gap depends on turn count increments at turn_end; mid-turn chunks do not advance gap counters.