From 24a3d35c062724c489da403d35dc4178b15508e8 Mon Sep 17 00:00:00 2001 From: GRayHook Date: Thu, 18 Jun 2026 22:59:59 +0700 Subject: [PATCH] Phase 15: add mid-turn defer reinject via sendMessage steer Deliver pending skills on session_compact when agent is not idle, and skip before_agent_start consume when steer already ran for compaction. Co-authored-by: Cursor --- src/compaction.ts | 6 ++++++ src/reinject.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/compaction.ts b/src/compaction.ts index 84a2223..d8c1527 100644 --- a/src/compaction.ts +++ b/src/compaction.ts @@ -8,6 +8,10 @@ export interface CompactionRuntime { clearPendingReinjectOnNextUserInput: boolean; /** Last compact firstKeptEntryId for debug kept-window snapshots (Phase 14). */ lastCompactionFirstKeptEntryId: string | null; + /** Last compaction entry id on session_compact (Phase 15 / B-003 dedup). */ + lastCompactionEntryId: string | null; + /** Compaction id when mid-turn steer already delivered reinject (Phase 15 / §6.5.1). */ + deferredDeliveredForCompactionId: string | null; } export function createCompactionRuntime(): CompactionRuntime { @@ -15,6 +19,8 @@ export function createCompactionRuntime(): CompactionRuntime { pendingCompactionSource: null, clearPendingReinjectOnNextUserInput: false, lastCompactionFirstKeptEntryId: null, + lastCompactionEntryId: null, + deferredDeliveredForCompactionId: null, }; } diff --git a/src/reinject.ts b/src/reinject.ts index 1ebba1d..15aa079 100644 --- a/src/reinject.ts +++ b/src/reinject.ts @@ -352,6 +352,46 @@ export function filterPendingReinjectForConsume( return resolved; } +/** + * Mid-turn defer delivery via sendMessage/steer (SPEC §6.5.1, Phase 15 / B-003). + * Clears pendingReinject and records compaction id so before_agent_start does not duplicate. + */ +export function deliverDeferredReinjectSteer( + pi: ExtensionAPI, + state: ExtensionState, + settings: SkillReinjectSettings, + registeredSkills: readonly Pick[], + compactionRuntime: CompactionRuntime, + compactionEntryId: string, + ctx?: ExtensionContext, +): boolean { + if (state.pendingReinject.length === 0) { + return false; + } + const pendingNames = filterPendingReinjectForConsume( + state.pendingReinject, + state, + settings, + registeredSkills, + ctx, + ); + const content = buildDeferredReinjectContent(pendingNames, state, settings, registeredSkills, ctx); + state.pendingReinject = []; + if (!content) { + return false; + } + pi.sendMessage( + { + customType: DEFERRED_REINJECT_CUSTOM_TYPE, + content, + display: true, + }, + { deliverAs: "steer" }, + ); + compactionRuntime.deferredDeliveredForCompactionId = compactionEntryId; + return true; +} + /** * Defer path on before_agent_start: inject one combined message, then clear queue (SPEC §6.5.1). * Returns undefined when pendingReinject is empty or manual compaction scheduled a clear (SPEC §16.5). @@ -366,6 +406,12 @@ export function tryConsumeDeferredReinject( if (compactionRuntime?.clearPendingReinjectOnNextUserInput) { return undefined; } + if ( + compactionRuntime?.deferredDeliveredForCompactionId && + compactionRuntime.deferredDeliveredForCompactionId === compactionRuntime.lastCompactionEntryId + ) { + return undefined; + } if (state.pendingReinject.length === 0) { return undefined; }