* Trigger evaluation + fire-and-forget historian invocation. Runs * after the synchronous tagging pass so trigger logic sees the * just-assigned tags. * * The actual historian subagent spawn (`runPiHistorian`) is async * and intentionally NOT awaited — the LLM call should never wait on * histo
(args: {
ctx: ExtensionContext;
sessionId: string;
db: ContextDatabase;
historian: PiHistorianOptions;
isFirstContextPassForSession?: boolean;
activeTags?: ReturnType<typeof getActiveTagsBySession>;
rawMessageProvider?: {
readMessages: () => ReturnType<typeof readPiSessionMessages>;
};
})
| 2978 | * agent turn continues regardless of historian outcome. |
| 2979 | */ |
| 2980 | function maybeFireHistorian(args: { |
| 2981 | ctx: ExtensionContext; |
| 2982 | sessionId: string; |
| 2983 | db: ContextDatabase; |
| 2984 | historian: PiHistorianOptions; |
| 2985 | isFirstContextPassForSession?: boolean; |
| 2986 | activeTags?: ReturnType<typeof getActiveTagsBySession>; |
| 2987 | rawMessageProvider?: { |
| 2988 | readMessages: () => ReturnType<typeof readPiSessionMessages>; |
| 2989 | }; |
| 2990 | }): void { |
| 2991 | const { ctx, sessionId, db, historian, isFirstContextPassForSession } = args; |
| 2992 | |
| 2993 | if (inFlightHistorian.has(sessionId)) { |
| 2994 | sessionLog(sessionId, "historian trigger eval: in-flight, skipping"); |
| 2995 | return; |
| 2996 | } |
| 2997 | |
| 2998 | // Prefer OpenCode-equivalent pressure persisted by message_end. |
| 2999 | // Pi's built-in `ctx.getContextUsage()` reports total-tokens |
| 3000 | // percent (input + output + cache), but historian/trigger math |
| 3001 | // expects wire-input pressure (input + cacheRead + cacheWrite). |
| 3002 | // `session_meta.lastContextPercentage` carries the corrected value |
| 3003 | // computed by `pi-pressure.ts` against the effective context |
| 3004 | // limit (with detected_context_limit override applied). |
| 3005 | let usage: { percentage: number; inputTokens: number }; |
| 3006 | let usageContextLimit: number | undefined; |
| 3007 | try { |
| 3008 | const piUsage = ctx.getContextUsage?.(); |
| 3009 | let usageSource: "session_meta" | "piUsage fallback"; |
| 3010 | // Sane-bound (isSaneLimit, NOT `> 0`) so a garbage-but-positive window |
| 3011 | // can't drive the trigger budget — mirrors the main pressure pass. |
| 3012 | usageContextLimit = isSaneLimit(piUsage?.contextWindow) |
| 3013 | ? piUsage.contextWindow |
| 3014 | : undefined; |
| 3015 | // Cold-start: fall back to the model's window when usage hasn't reported |
| 3016 | // a sane one yet (first pass after restart). |
| 3017 | if ( |
| 3018 | usageContextLimit === undefined && |
| 3019 | isSaneLimit(ctx.model?.contextWindow) |
| 3020 | ) { |
| 3021 | usageContextLimit = ctx.model.contextWindow; |
| 3022 | } |
| 3023 | // Apply the detected-overflow cap (authoritative real limit) just like the |
| 3024 | // main pass — otherwise the trigger budget uses the wrong (larger) limit. |
| 3025 | try { |
| 3026 | const overflowState = getOverflowState(db, sessionId); |
| 3027 | if (overflowState.detectedContextLimit > 0) { |
| 3028 | usageContextLimit = Math.min( |
| 3029 | usageContextLimit ?? overflowState.detectedContextLimit, |
| 3030 | overflowState.detectedContextLimit, |
| 3031 | ); |
| 3032 | } |
| 3033 | } catch { |
| 3034 | // Best-effort — fall through with the uncorrected limit. |
| 3035 | } |
| 3036 | const sessionMetaForUsage = getOrCreateSessionMeta(db, sessionId); |
| 3037 | if ( |
no test coverage detected