fix: skip trail context injection for review sessions · Entire

fix: skip trail context injection for review sessions

cc43e1e→main·

dipree·1mo ago·3 files·+15 added/-5 removed

- Skip ContextInjector emission for review/investigate sessions (state.Kind != "") so they don't pay the trails-enabled probe or spend context tokens on a branch-tracking hint. - Make the accepted first-turn tradeoffs explicit in comments: the probe is synchronous so the hint can reach the first model call, bounded by timeout, and transient probe failures fail closed for that session. - Update the ContextInjector architecture note to match the state-field based once-per-session decision.

Sessions

c27e1c8eb9e9View transcript

?\Inject Trail Context into Agent ModelPi·Opus 4.8·4 steps

Changes

3

414 unmodified lines

415
416
417
418
419
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435

414 unmodified lines

// Decide once per session, recorded on the session state itself (not a
    // separate marker file). Winning the check-and-set means this turn owns the
    // decision; the trails-enabled probe (a network call) then runs at most once
    // per session. Marking "decided" even when trails are disabled stops later
    // turns from re-probing, since enablement is stable for the session.
    // per session. This is intentionally synchronous so the hint can reach the
    // first model call; trailsEnabledForRepo bounds the whole decision path to a
    // short timeout. Marking "decided" before probing means transient probe
    // failures fail closed (no hint for this session) rather than retrying/spamming.
    won := false
    mutErr := strategy.MutateSessionState(ctx, event.SessionID, func(state *strategy.SessionState) error {
        if state.ContextInjectionDecided {
            return strategy.ErrMutationSkip
        }
        // Review/investigate sessions are task-specific and don't need the branch
        // trail pointer; skip without marking decided so normal sessions keep the
        // usual first-turn behavior.
        if state.Kind != "" {
            return strategy.ErrMutationSkip
        }
        state.ContextInjectionDecided = true
        won = true
        return nil
    }

Mcmd/entire/cli/lifecycle.go+10/-2

227 unmodified lines

228
229
230
231
232
231
232
233
234
235
236
237

227 unmodified lines

// ContextInjectionDecided records that the once-per-session model-context
    // injection (e.g. the `entire trail` pointer) has been handled for this
    // session, so the dispatcher neither re-probes the API nor re-injects on
    // later turns. Set on the first turn regardless of whether anything was
    // injected, since trail enablement is stable for the session.
    // later turns. Set on the first normal turn regardless of whether anything
    // was injected: trail enablement is stable for the session, and transient
    // probe failures fail closed (miss the hint) rather than retrying/spamming.
    // Review/investigate sessions leave this false because they skip injection.
    ContextInjectionDecided bool `json:"context_injection_decided,omitempty"`

// AgentType identifies the agent that created this session (e.g., "Claude Code", "Gemini CLI", "Cursor")

Mcmd/entire/cli/session/state.go+4/-2

50 unmodified lines

51
52
53
54
54
55
56
57

50 unmodified lines

| `TokenCalculator` | `CalculateTokenUsage` | Agent's transcript contains token usage data |
| `SubagentAwareExtractor` | `ExtractAllModifiedFiles`, `CalculateTotalTokenUsage` | Agent spawns subagents (like Claude Code's Task tool) |
| `HookResponseWriter` | `WriteHookResponse` | Agent can display messages from hook responses (e.g., session start banner). Claude Code uses JSON `systemMessage` on stdout; Factory AI Droid uses plain text on stdout. |
| `ContextInjector` | `InjectionEvent`, `RenderContextInjection` | Agent can inject text into the **model's** context window (distinct from `HookResponseWriter`, which targets the *user*). The agent declares which lifecycle event it injects at and renders a native stdout payload. The dispatcher (`emitContextInjection`) emits it once per session via `strategy.ClaimContextInjection`, and only when trails are enabled for the repo on the API (`trailsEnabledForRepo` → `api.Client.TrailsEnabled` probes the trails endpoint; the claim is taken first so the probe runs at most once per session). Claude Code / Codex / Gemini inject at `TurnStart` using `hookSpecificOutput.additionalContext` (UserPromptSubmit / BeforeAgent); Pi and OpenCode emit a `{"inject_context":...}` envelope that their embedded extension applies (Pi via a `before_agent_start` message, OpenCode via `experimental.chat.system.transform`). |
| `ContextInjector` | `InjectionEvent`, `RenderContextInjection` | Agent can inject text into the **model's** context window (distinct from `HookResponseWriter`, which targets the *user*). The agent declares which lifecycle event it injects at and renders a native stdout payload. The dispatcher (`emitContextInjection`) emits it once per normal session via `session.State.ContextInjectionDecided`, skipping review/investigate sessions, and only when trails are enabled for the repo on the API (`trailsEnabledForRepo` → `api.Client.TrailsEnabled` probes the trails endpoint). The decision is taken before probing so the synchronous first-turn probe runs at most once per session and transient probe failures fail closed (no hint). Claude Code / Codex / Gemini inject at `TurnStart` using `hookSpecificOutput.additionalContext` (UserPromptSubmit / BeforeAgent); Pi and OpenCode emit a `{"inject_context":...}` envelope that their embedded extension applies (Pi via a `before_agent_start` message, OpenCode via `experimental.chat.system.transform`). |
| `FileWatcher` | `GetWatchPaths`, `OnFileChange` | Agent doesn't support hooks; uses file-based detection instead |

## Step-by-Step Implementation Guide

Mdocs/architecture/agent-guide.md+1/-1