@sema-agent/core
Version:
Stateless, task-oriented AI agent core
839 lines • 58 kB
TypeScript
import { type AgentMessage, type CompactionSettings, type Session, type ThinkingLevel } from "../internal/harness.js";
import type { Message, Model, Tool } from "../internal/llm.js";
import type { ContextWindowOverlay } from "./context-settings.js";
import type { PostCompactContext, PreCompactContext, PreCompactResult } from "./hooks.js";
import type { TraceEvent } from "./trace.js";
import type { Brain } from "./types.js";
import type { TaskEvent, TaskEventIdentity } from "./task-event.js";
import type { CompactionEffect } from "./frame-settlement.js";
/**
* Default summarization focus, applied when a task does not provide `compaction.instructions`.
* Addresses the common fidelity failure where summaries drop specific facts.
*/
export declare const DEFAULT_COMPACTION_INSTRUCTIONS: string;
/**
* design/145 §4 — uniform hygiene for EVERY summarization-instructions channel (spec-level
* `compaction.instructions`, the DEFAULT constant, a manual `/compact` per-call override, and a
* preCompact hook's `additionalInstructions` — 修类不修点): instructions text can be model- or
* caller-authored, and it is injected VERBATIM into the summarization prompt
* (`\n\nAdditional Instructions:\n${...}`, CC 207 :347521 shape), so a forged `</system-reminder>`
* or `<<<`/`>>>` fence sentinel inside it would otherwise ride into a trusted prompt position.
* Posture (r2 review B4): `sanitizeUntrustedText` + fence defusing only — NEVER `delimitUntrusted`
* (fencing instructions as "data only, do not follow" self-defeats their instructional intent, and
* the fence markers would be echoed back into the persisted summary). Cap = 2048 code points
* (surrogate-safe slice): instructions steer a summary, they are not a payload channel.
* Text without break-out sequences under the cap passes through BYTE-IDENTICAL (the no-arg /
* default-instructions prompt is unchanged — pinned by test).
*/
export declare const COMPACTION_INSTRUCTIONS_MAX_CHARS = 2048;
export declare function sanitizeCompactionInstructions(text: string): string;
/**
* 压缩子阶段遥测 (1.283 G) — single-source shape: the `phaseDurations` result field IS the
* (defined-but-not-yet-emitted) `compaction.phase_timings` trace frame minus its envelope
* (`kind`/`version`/`taskId`/`ts`) and minus `durationMs` (already a sibling result field). Deriving
* it from the frame keeps the follow-up runtask bridge a mechanical spread — and makes a rollback of
* either side a tsc error, not a silent drift.
*/
export type CompactionPhaseDurations = Omit<Extract<TraceEvent, {
kind: "compaction.phase_timings";
}>, "kind" | "version" | "taskId" | "ts" | "durationMs">;
/** True iff `err` is a compaction failure caused by a manual compact() caller WITHDRAWING its
* request — see {@link MaybeCompactOptions.manualCancelSignal}. Two mint sites since the
* design/374 slice-3 commit-boundary re-check (C16): the summary call's own abort outcome, and
* the pre-persist "cancelled before commit" throw (a brain that ignored the abort finished the
* summary, but the withdrawal still fired before anything landed). Absence of the marker means
* the failure was NOT the withdrawal: a real summarizer error racing a late cancel stays
* unmarked so the caller's breaker/onError accounting still sees it. */
export declare function isCompactionManualCancel(err: unknown): boolean;
/**
* design/145 §1/§3 — the window-safety decision surfaced to the caller BEFORE the summary call
* (same pre-call posture as `onInputTruncated`: the disclosure must survive a later summary
* failure). `kind:"fallback"` → the summary call was moved to the MAIN model (window safety);
* `kind:"clamp_disclosure"` → the small model keeps the call but the conversation WILL be
* clamp-truncated: `reason` says why the fallback did not happen ("budget" = the gate
* denied it; "tolerance" = the truncation is within `clampTolerance` — bounded, but no longer
* silent). All token figures are in the MAIN model's structural coordinate except
* `headroomTokens` (the小 model's — it describes THAT window's capacity).
*/
export interface CompactionWindowSafetyInfo {
kind: "fallback" | "clamp_disclosure";
truncationRatio: number;
/** Present on "clamp_disclosure": why the small model kept the call despite the truncation. */
reason?: "budget" | "tolerance";
/** Serialized conversation size incl. any re-sent previousSummary (chars ÷ main-model charsPerToken). */
contentTokens: number;
/** ≈ what fits in the compaction model's window (kept chars ÷ its charsPerToken); estimate. */
headroomTokens?: number;
/** 门A estimate: main-model input price × contentTokens, integer micro-USD (absent when pricing
* was unavailable — the gate then never blocked on budget). */
estCostMicroUsd?: number;
}
/**
* SEMA-ONLY (trim-seam defense, no CC analogue) — stale-anchor freshness margin: a usage anchor is
* trusted only while the anchored estimate stays within `structural × margin + overhead` of the
* current message list. 2 covers the worst measured structural under-count (charsPerToken=3 vs a
* measured ~2.18 true ratio ⇒ ≤ ~1.4x) plus reasoning-block accounting slop, while still catching
* the live deception band (anchored estimates 2.9x–18x over the real context — docs/TOKEN-ESTIMATE-
* CALIBRATION-LIVE-2026-07-09.md §3/§4).
*/
export declare const STALE_ANCHOR_STRUCTURAL_MARGIN = 2;
export declare function sanitizeCompactionSettings(settings: CompactionSettings, contextWindow: number | undefined): CompactionSettings;
/**
* design/169-A — the CC-form fork context: the main lane's latest REAL brain request (same
* `systemPrompt` bytes, same message-list bytes, same tools). The summary request built from it is
* that request plus ONE appended user-role instruction message, so the provider's prompt cache
* re-serves the already-paid main prefix — the economics that make "summarize on the main model"
* cheap. Structural twin of the engine-side `CompactionForkContext` (engine/compaction/compaction.ts
* — the engine layer cannot import core, so the shape is declared on both faces and must stay
* assignable; `compact()`'s parameter type is the checked seam).
*/
export interface CompactionForkContext {
systemPrompt?: string;
/** The physical cache-span block layout of the main request, when it had one (Context.systemBlocks)
* — the fork must reproduce it or a multi-block pack's cache boundaries are silently lost. */
systemBlocks?: Array<{
text: string;
cacheControlBoundary: boolean;
}>;
messages: Message[];
tools?: Tool[];
/** The model id that served the recorded request — part of the provider cache key; the fork
* eligibility gate refuses a snapshot from a different model (see the engine-side twin's doc). */
modelId?: string;
}
/**
* What a compaction pass reports at the moment it commits ({@link MaybeCompactOptions.onStarted}) — and the
* whole content of its `compaction_started` wire frame. `pass` is minted HERE, by the one function every lane
* runs, so no lane can number its own passes; the frame that ends the pass carries the same value.
*/
export interface CompactionPassStart {
readonly pass: string;
/** The trigger this pass runs under — the value it hands the `preCompact` hook. */
readonly trigger: "auto" | "manual" | "forced";
/** Engine clock, epoch ms, at the commit. */
readonly startedAt: number;
}
export interface MaybeCompactOptions {
/** Disclosure channel for CONTAINED host-callback failures in this scope (the RB-463
* isolation primitive's bounded first-failure per site). Absent ⇒ failures are counted only. */
onNotifyError?: (failure: import("./safe-notify.js").SafeNotifyFailure) => void;
/** The session to compact. Typed on the INTERFACE face (typecheck 车 2026-07-24): every member this
* module touches lives on `Session` (the optional `getPromptEpoch` degrades honestly via `?.()`). */
session: Session;
/** This run's prompt identity inputs (and the pinned center layer's provenance): the compaction restates the
* session's epoch when the identity is unchanged and advances it otherwise — the same identity the prepare-time
* record names, so a compaction never flaps the record. Absent ⇒ no restatement (a host compacting outside a run). */
promptIdentity?: {
readonly layers: import("../prompt-assembly/epoch.js").PromptIdentityLayers;
readonly sourceRevision?: string;
};
/** A compaction-boundary CENTER LAYER CHANGE, pre-assembled by the caller (prepare's seam): an adopted candidate,
* or an emptied center layer after an explicit revocation. The epoch advances to the identity after the change in
* the SAME CAS append as the new baseline, and `apply()` (the live prefix / fingerprint swap) runs ONLY after that
* append succeeded — a failed or blocked compaction swaps nothing. */
centerAdoption?: {
/** The run's identity inputs after the change (`layers.center` absent ⇒ the center layer is emptied). */
promptIdentity: {
readonly layers: import("../prompt-assembly/epoch.js").PromptIdentityLayers;
readonly sourceRevision?: string;
};
/** the post-swap prompt overhead in the trigger estimate's coordinate: used for THIS
* compaction's post-measurement so PTL recovery/anti-thrash see the swapped prefix size. */
overheadTokensAfter: number;
apply: (committedIdentity: string) => void;
};
/** Model whose context window bounds the budget (the model that runs the next turn). */
model: Model;
/** The deployment's LIVE per-model window overlay for THIS pass ({@link RunnerDeps.contextSettings}
* keyed by the leg's catalog word). Read by the caller at the moment of the pass, never snapshotted:
* every window number below — the trigger, the sanitize clamp, the disclosed `contextUsage` — is
* measured through it, so an operator who lowers a model's window reaches the legs already running.
* Absent ⇒ the model card's own declarations, unchanged. */
contextOverlay?: ContextWindowOverlay;
/** Optional cheaper model used to produce the summary. Falls back to `model`.
* design/145 §1 (semantics精确化): used only while WINDOW-SAFE and the budget gate allows — when a
* dry-run clamp shows the conversation would lose more than `settings.clampTolerance` of its
* chars to fit this model's window, the summary call falls back to the MAIN model (`model`),
* which is always window-safe (the content grew inside its window; CC 207 :348496 compresses
* with the main-loop model unconditionally — the fallback IS the CC posture, the independent
* compaction model is sema's superset). Subagent note (design/145 §5): delegated children never
* inherit this option (subagent.ts buildChildSpec carries no compactionModel), so a child always
* self-compacts on its own main model — the judgment is structurally never triggered there. */
compactionModel?: Model;
/**
* design/169-A — accessor for the main lane's latest real request context (see
* {@link CompactionForkContext}). Consulted per pass, at call time (the ref advances with every
* main request). Used ONLY when the summary call runs on the MAIN model (no `compactionModel` in
* play — an independent small model has no main prefix to share); `undefined` (or an accessor
* returning `undefined`: no main request issued yet, or a library caller driving maybeCompact
* directly) keeps the independent request form byte-for-byte. A fork-form prompt-too-long falls
* back ONCE to the independent form inside `compact()` (the fork form must not clamp — a clamp
* rewrites the cached prefix, defeating the form's purpose).
*/
forkContext?: () => CompactionForkContext | undefined;
brain: Brain;
getApiKeyAndHeaders?: (model: Model) => Promise<{
apiKey: string;
headers?: Record<string, string>;
} | undefined>;
thinking?: ThinkingLevel;
settings?: CompactionSettings;
/** Appended to the summarization prompt to steer what must be preserved. Sanitized inside
* maybeCompact via {@link sanitizeCompactionInstructions} (design/145 §4 — every channel,
* including the preCompact hook's additions, gets the same treatment). */
customInstructions?: string;
/**
* The user's / project's own INSTRUCTION-FILE content (the CLAUDE.md family this
* deployment loads into an ordinary session), for the INDEPENDENT summary form.
*
* Why it needs a seat at all: the summarization prompt carries CC's closing invitation to follow
* "additional summarization instructions provided in the included context", and the independent
* form swaps in its own system prompt — so the loader-mounted files never reached that
* summarizer and the invitation named nothing this form delivers. Supplying this makes it true:
* the content is rendered as the request's first section, ahead of the conversation, with
* instruction standing for the summarization task only. The FORK form carries it too — in the ONE
* message that form appends, so the cached main prefix is untouched: it preserves whatever system
* prompt the main request had, and nothing establishes that this prompt contains the files, so a
* conforming fork response would otherwise succeed having never seen them. Budget for the seat's
* bytes on BOTH forms.
*
* TRUST TIER — host supply, same as `systemPrompt`: this string is handed to the model as
* instructions. Never fill it from tool output, model text, or any other in-band channel. It is
* NOT sanitized or length-capped here (unlike `customInstructions`, which is per-pass steering
* text of unknown provenance) precisely because it is the deployment's own prompt material.
*
* ABSENT (default) ⇒ every summarization request byte is what it was before this option existed.
* That invariant is over the summarization REQUEST; the one deliberate effect outside it — the
* new `<instruction-files>` authority envelope being defused on the fenced/shelled lanes for every
* deployment — is stated at `CompactionPreparation.contextInstructionFiles`.
*/
contextInstructionFiles?: string;
signal?: AbortSignal;
/**
* The pass's WITHDRAWAL signal — the run's caller-stop gate (the stop verbs and the task's own
* `signal`), composed with the cancel signals of the manual callers THIS pass claimed (a SUBSET
* of `signal`, which may additionally carry the leg's own abort).
* Used only for failure ATTRIBUTION: when the summary call's failure is its abort outcome and
* this signal has fired, the thrown error is stamped so {@link isCompactionManualCancel} holds —
* the caller then treats the pass as withdrawn ("mooted") instead of summarizer evidence. It
* never aborts anything by itself (the same signals already ride inside `signal`).
*/
manualCancelSignal?: AbortSignal;
/**
* design/145 门A — budget interaction for the window-safety fallback: a MAIN-model re-compaction
* of a near-window conversation is one giant prefill whose cost lands in the SAME ledger the
* budget gate enforces (`recordCompactionUsage` → `stats.costMicroUsd` → `overBudget()`), so an
* un-gated fallback under a cost cap is the "钱花了任务照死" double loss. When present AND
* `mainInputPer1M` is known, the fallback is denied if `spentMicroUsd() + contentTokens ×
* mainInputPer1M > capMicroUsd` — the pass then keeps the small model and DISCLOSES the clamp
* (`clamp_disclosure`, reason "budget"). Absent field / absent pricing = the gate never blocks
* (design: "pricing/cap 不可得=门不拦").
*/
fallbackBudget?: {
/** Live cumulative spend reader (the caller's `stats.costMicroUsd` at decision time). */
spentMicroUsd: () => number;
/** The run's cost ceiling, integer micro-USD. */
capMicroUsd: number;
/** MAIN-model input price, absolute USD per 1M tokens (`ModelPricing.inputPer1M`) — numerically
* equal to micro-USD per token, so `est = contentTokens × mainInputPer1M` needs no unit dance. */
mainInputPer1M?: number;
};
/**
* design/145 §3 — pre-call observer for the window-safety decision (fallback / disclosed clamp).
* Fired BEFORE the summary call (survives a later failure, `onInputTruncated` posture); throws
* are swallowed (observe-only). The caller bridges it to the `compaction.model_fallback` /
* `compaction.clamp_disclosure` trace frames.
*/
onWindowSafety?: (info: CompactionWindowSafetyInfo) => void;
/**
* Anti-thrash re-trigger floor (design/64 §25.2): ALSO require the estimated context to exceed this
* many tokens before compacting. The within-task caller sets it after each compaction (post-compaction
* size × regrowth factor) so a compaction that could not free much headroom — small window, chunky
* turns, large summary — does not re-fire a real summary LLM call at every subsequent boundary
* (LONGRUN-1b measured 26/39 boundaries firing, +135% wall, without it). 0/undefined = no floor.
*/
minTokens?: number;
/**
* Force a compaction even when context is UNDER the auto-threshold (and ignore `minTokens`) — the
* manual `/compact` path (TaskStream.compact, shell K-1c). It bypasses ONLY the threshold gate, not the
* structural floor: if there is no valid cut point (too little history to summarize) the call still
* returns `{compacted:false}` quietly. Default `false` = the auto-threshold policy.
*/
force?: boolean;
/**
* design/123 D4 (r5 final discriminant) — trim→compaction pressure propagation: force a compaction
* whenever the NATURAL gate would not fire, i.e. `est < max(threshold, minTokens-floor)`. Trim
* dropping real messages is external hard evidence that compaction is overdue, and it must override
* BOTH gates that can wrongly hold it back:
* - est < threshold — the deceived-estimate regime (request-only trim deflates the next usage
* anchor while the session stays full-size; the 16k-live sawtooth), and
* - threshold ≤ est < floor — the floor-blocked regime (post-compaction §25.2 floor above an
* HONEST over-threshold estimate; 5th live run: 27 boundaries trim-dropping while the floor
* held compaction off and input climbed 16k→27.4k).
* `est ≥ max(threshold, floor)` fires naturally anyway (no force needed/applied — the result then
* reports `naturalTrigger: true`). Spiral protection lives in the CALLER via
* {@link nextTrimForceBackoff}: any landed compaction that still posts over the threshold
* (incompressible transcript, the LONGRUN-1b shape) disables further trim-forces until a landed
* compaction posts under the threshold again. Like `force`, this never bypasses the structural
* floor (no valid cut point ⇒ `{compacted:false}`).
*/
forceUnderThreshold?: boolean;
/**
* Working-file attachments (LONGRUN-2, CC-compact parity review 2026-06-12): after a successful
* summary, re-read up to `maxFiles` of the files the summarized region MODIFIED (from
* CompactionDetails.modifiedFiles) and append their current on-disk contents to the summary text.
* Why: LONGRUN-2 measured the model spending ≈2.3 extra read calls per compaction re-fetching
* files it had just lost — each one a full request round-trip at post-compaction context size.
* CC ships the same idea ("attachments", ≤5 files × ~5k tokens). Bounded: per-file char cap plus
* a total cap that self-scales to ~15% of the model window, so small windows never drown in
* attachments; the §25.2 freedTokens measurement naturally accounts for the added mass (the
* anti-thrash floor stays truthful). Reader failures skip the file (best-effort, never fails the
* compaction). Absent (default) = current behavior, paths-only.
* Pick order (design/71 P2-① resolved the LONGRUN-2b limitation): `maxFiles` picks the most
* RECENTLY touched files first (`CompactionDetails.modifiedFilesByRecency`, recency threaded
* through the engine fileOps accumulator); alphabetical `modifiedFiles` remains the fallback for
* details persisted before the field existed.
*/
workingFileAttachments?: {
/** Read a task workspace file; null/throw = skip it. Wired by the Runner from the task's ExecutionEnv. */
readFile: (path: string) => Promise<string | null | {
withheld: {
pattern: string;
};
}>;
/** Max modified files to attach. Default 3. */
maxFiles?: number;
/** Per-file char cap (≈4 chars/token). Default 16_000 (~4k tokens). */
maxCharsPerFile?: number;
/**
* CC-parity selection set: the files the task has READ, most recent
* first (CC 198's post-compact restore sorts readFileState by timestamp — the read set is wider
* than the modified set and includes reference files the model consulted). When provided and
* non-empty it WINS over `modifiedFilesByRecency`; absent/empty falls back to the modified set
* (the pre-2026-07-03 behavior, and the right answer for callers with no read tracking).
*/
recentlyReadFiles?: () => string[];
/**
* RB-197② (CC `compact.ts:1415-1463` `shouldExcludeFromPostCompactRestore` parity): paths already
* re-seeded through a DIFFERENT channel every turn (the deployment's declared instruction files —
* `ProjectMemoryLoad.instructionSources`, e.g. a project/user CLAUDE.md) are excluded from the
* `recentlyReadFiles`/fallback candidate set — re-attaching them here would just duplicate content
* the system-prompt lane already carries. A plain path list, not a hardcoded filename: core still
* never decides what counts as "an instruction file" on its own (types.ts's own seam rule) — it
* only trusts what the deployment already declared.
*/
instructionSourcePaths?: ReadonlyArray<string>;
/**
* `recentlyReadFiles` yields CANONICAL keys (resolveKey
* output — the same coordinate `ReadFileState` is keyed in), but `instructionSourcePaths` and the
* kept-tail scan inside `maybeCompact` (which reads raw tool-call arguments via
* `extractFileOpsFromMessage`) do NOT share that coordinate — a model-typed relative path and its
* canonical form are different strings, so a plain `Set.has()` between them silently never matches
* even for the identical file. Optional callback (present exactly when there's a containment root
* to resolve against — i.e. hands enabled) that canonicalizes a raw path the SAME way the hands
* toolkit does, so `maybeCompact` can normalize every side of the comparison before excluding.
* Best-effort: a throw/rejection is caught by the caller and treated as "leave this one path
* unnormalized" (a safe direction — worst case is a missed exclusion, not a wrong one). Absent
* when there is no containment root (hands disabled) — in that regime every path source is
* ALREADY in the same raw coordinate, so the fallback exact-string comparison is already correct.
*/
normalizePath?: (raw: string) => Promise<string>;
/**
* A kept-tail Read tool-CALL's mere presence does not prove the
* file's real content is visible there: its toolRESULT may be a dedup stub ("unchanged since you
* last read it…") whose ORIGINAL full transmission sits in the summarized prefix THIS compaction is
* about to cut away — preserving read-state for such a path would leave both the kept tail and the
* next read pointing at content that is nowhere in context (RB-197①'s failure, reintroduced one hop
* removed). Optional predicate (present exactly when the caller's Read tool actually emits a
* recognizable stub marker) so `maybeCompact` can require at least one SUBSTANTIVE kept-tail Read
* result before trusting a path as genuinely visible; absent ⇒ every non-error kept-tail Read is
* trusted (the pre-this-fix behavior — still safe when the caller has no stub concept at all).
*/
isDedupStubResult?: (resultText: string) => boolean;
};
/**
* Fires ONCE per LANDED compaction (after `appendCompaction` committed the new baseline), with the
* files that were attached WHOLE (untruncated) into the summary. The Runner wires this to the read-state
* invalidation (`applyContextShrinkToReadFileState`): the summary just replaced the history the read-dedup
* stubs point at, so stale entries must drop and the re-attached content re-registers. Kept OUTSIDE
* `workingFileAttachments` deliberately — the invalidation is owed even when attachments are disabled
* (an empty array then). Best-effort: a throwing callback must not fail the landed compaction.
*
* `preserveReadState`:
* canonical-coordinate paths (same shape as `normalizePath`'s output) whose read-state entry must
* survive the compaction's clear even though they are NOT in `attachedComplete` — every file
* `maybeCompact` deliberately skipped re-attaching, for EITHER exclusion reason (kept-tail-visible,
* or matches a declared instruction source). Without this the read-state clear (see
* `applyContextShrinkToReadFileState`) would wipe them anyway, defeating both the read-before-edit gate
* and the next read's dedup for exactly the files this optimization was supposed to leave untouched.
* (The instruction-source half of this was missing in an earlier revision: `instructionSources` and
* `seededFiles` are independent `ProjectMemoryLoad` fields — a path can be declared in the former
* without ever being seeded via the latter, so an instruction-excluded file the model explicitly Read
* has a live, non-seeded entry that needs the SAME preservation as a kept-tail one.)
*/
onApplied?: (attachedComplete: ReadonlyArray<{
path: string;
content: string;
}>, preserveReadState?: ReadonlyArray<string>) => void;
/**
* #1036 — THE COMMIT-BEAT EFFECT SINK: what this compaction did to the transcript, reported
* once, right after the `appendCompaction` that landed it. `summarizedEntryIds` = every entry id
* that fell into the summarized prefix (before `firstKeptEntryId`); `"unknown"` when the kept-tail
* anchor is not in the branch — the conservative reading (the same one
* {@link countSummarizedCrossSessionMessages} already takes) is that the whole branch went into
* the summary.
*
* Why HERE and not at the three call sites: a fact about a landed compaction has exactly one
* birthplace, and an effect minted per lane would be a rule written three times (and, the two
* times someone forgets, a lane where a summarized frame is silently never repaired). The three
* lanes reach it through the options assembly they already share.
*
* Observe-only, like every post-landing side effect here: a throwing sink must not fail a landed
* compaction.
*/
onCommitted?: (effect: CompactionEffect) => void;
/**
* env-tail migration — the git-status lane's compaction interaction (two-phase receipt). The git
* frame lives on the MESSAGE plane, does not enter the summary, and the projection strips it from
* serialized history — so a landed compaction would silently lose the model's git view while the
* announced hash keeps suppressing every re-send. When present:
* - `pending` is restated on the compaction entry's `details.gitAnnouncement` with
* `pending: true` in the SAME CAS as the new baseline (the promptEpoch/announcedListings
* restatement posture) — any reader between the landing and the re-assert receipt sees an
* OWED announcement, never a false "announced";
* - `land()` is invoked immediately after the landed baseline, BEFORE any context rebuild or
* retry (all three routes — turn-boundary auto, prompt-too-long recovery, end-of-task finish
* — funnel through this one append): it re-appends the current frame and writes the
* announced mirror once the append receipt is in hand. `land` contains its own failures (a
* failed re-assert leaves the pending restatement authoritative — the next boundary/leg
* re-announces); a throw here must never fail a LANDED compaction, so the call is belted.
*/
gitRestate?: {
pending: {
kind: "full" | "degraded" | "unavailable" | "non-repo";
hash: string;
};
land: () => Promise<void>;
};
/** The approval-posture directive's post-compaction restatement (the runner's `compactionRestateOptions`):
* runs right after `gitRestate.land`, under the same belt and for the same reason — the compacted
* context may have lost an engine frame the run still needs, and every reader of the new baseline
* (the next model request first) must see it restated. */
postureRestate?: () => Promise<void>;
/**
* Fixed per-request prompt overhead (system prompt + tool schemas, in tokens), applied ONLY in the
* anchor-less regime (design/64 §26.7, search [91] fourth candidate): when no assistant in the
* visible history carries real usage (custom Brain implementations that don't report usage), the
* chars/4 fallback counts MESSAGES only and is blind to this overhead — the threshold would compare
* apples to oranges and under-trigger. When usage-anchored, the anchor IS the provider-billed input
* (system+tools included), so the overhead is already in and this is not added.
*/
overheadTokens?: number;
/**
* design/84 Seam C — compact-boundary COST optimization (data-gated). Fidelity is already achieved by
* the iterative-merge UPDATE_SUMMARY path (compaction.ts); the only remaining cost is one LLM call per
* boundary. A `summaryProvider` lets a caller supply a summary WITHOUT an LLM call: when it returns a
* NON-EMPTY string, that string is reused verbatim as the summary (the real `generateSummary` LLM call
* is skipped, `reused:true`). Returning `null`/`undefined`/an empty-or-whitespace string falls back to
* the existing `generateSummary` path — IDENTICAL to current behavior. **Absent (default) = no behavior
* change whatsoever**: the LLM path runs byte-for-byte as before.
*
* THIS IS A THIN SEAM ONLY: no provider is implemented in core. The provider implementation is gated on
* design/82 cost-per-truly-correct data (§3/§7-5). Reuse is guarded against degradation (see below).
*
* Inputs:
* - `messagesToSummarize`: the message range that would be summarized this boundary.
* - `previousSummary`: the prior compaction's summary (present on the UPDATE/iterative-merge path).
* - `modifiedFiles`: files the summarized range modified, newest-first (same source the working-file
* attachments use), so a provider can decide whether the reused summary is still representative.
*/
summaryProvider?: (input: {
messagesToSummarize: AgentMessage[];
previousSummary?: string;
modifiedFiles: string[];
}) => string | null | undefined | Promise<string | null | undefined>;
/**
* design/84 Seam C reuse guard (c): after this many CONSECUTIVE provider-reuse compactions, the next
* compaction is FORCED through the real `generateSummary` LLM path even if the provider would hit — this
* bounds summary drift from indefinite reuse. The caller owns the counter via {@link onCompaction}
* (`reused`) and feeds it back via this option; core treats `summaryProvider` as if absent for that one
* boundary when `consecutiveProviderReuse >= maxConsecutiveProviderReuse`. Default `3`, a
* SEMA choice — the earlier "(CC parity)" label was unsupported and is withdrawn (anchoring
* re-check 2026-08-23: CC 2.1.223 has no external summary provider and no reuse concept at all;
* its three 3s are the PTL retry cap `b$d`, the consecutive-compaction-failure breaker `C$d` and
* the rapid-refill trip `Q3u`, none of them this). 3 is picked for the same reason those are: a
* small bound on how long a degraded path may keep answering before the real one is forced.
* `0`/undefined with no `consecutiveProviderReuse` = provider always consulted (no forced refresh).
*/
maxConsecutiveProviderReuse?: number;
/**
* design/84 Seam C reuse guard (c) — counter input: how many consecutive provider-reuse compactions have
* happened so far (caller-tracked from {@link onCompaction}'s `reused`). When this reaches
* `maxConsecutiveProviderReuse`, the provider is bypassed for this one boundary to force a real summary.
*/
consecutiveProviderReuse?: number;
/**
* design/84 Seam C — pure observer fired AFTER a successful compaction (never on a no-op/under-threshold
* return). Does NOT alter control flow. `reused` distinguishes a provider-supplied summary from a real
* LLM `generateSummary`. `freedTokens` is the §25.2 structural delta (may be ~0 when a reused summary
* frees little — see reuse guard (b): such a compaction does NOT raise the caller's anti-thrash floor).
* ABSENT when the post measurement failed — never a stand-in ("fully effective" is not true of a
* landing that leaves a summary behind). Every consumer reads absence as "no evidence": the floor
* stays where it is and the backoff does not flip.
*/
onCompaction?: (info: {
messagesCompacted: number;
freedTokens?: number;
tokensBefore: number;
reused: boolean;
}) => void;
/**
* design/134 §3.2 — which trigger this pass runs under, threaded into the pre/postCompact callback
* contexts and driving the block semantics: "auto" (threshold/boundary, the default) | "manual"
* (/compact) | "forced" (promptTooLong recovery and trim-pressure propagation — the compaction is
* not optional there, so a preCompact `block` is IGNORED on "forced").
*/
trigger?: "auto" | "manual" | "forced";
/**
* The pass has COMMITTED: the trigger gate passed, a valid cut point exists and no `preCompact` handler
* blocked it, and nothing has yet consulted the summary provider or called the model. Fired exactly once per
* pass — a retry inside the summary call is the same pass — and never for a pass that ends before this point
* (under threshold, held under the floor, disabled, nothing to compact, blocked, or cut by its own signal while
* the `preCompact` hook ran). A pass that fired it ends
* in `compacted: true` or a throw, so the caller that put a frame on the wire here owes the matching end.
* Observe-only: a throw — or the rejection of an `async` callback, which this seat does not await — is
* contained and never reaches the pass.
*/
onStarted?: (start: CompactionPassStart) => void;
/**
* design/134 (CC PreCompact parity) — fired AFTER the trigger gate and a CONFIRMED valid cut point
* (so every invocation corresponds to a compaction that would actually happen), BEFORE the
* provider/summary call. `block` skips this compaction on auto/manual (`{compacted:false,
* blocked:true}`); ignored on "forced". `additionalInstructions` is APPENDED to the summarization
* instructions (never replaces them). A throw is swallowed on every path — treated as
* no-block/no-instructions (the caller's wrapper owns tracing; an observer bug must never feed the
* caller's breaker or kill PTL recovery).
*/
preCompact?: (ctx: PreCompactContext) => PreCompactResult | undefined | Promise<PreCompactResult | undefined>;
/**
* design/134 (CC PostCompact parity) — observe-only, fired after the compaction LANDED
* (appendCompaction done, post measurement taken). A throw is swallowed (same isolation contract
* as {@link onCompaction}: the compaction already succeeded).
*/
postCompact?: (ctx: PostCompactContext) => void | Promise<void>;
/**
* MF-18 修② — observe-only: fired when a summary call's serialized conversation INPUT was
* truncated to fit the compaction model's context window (the ~300K-session shape that previously
* guaranteed a provider prompt-too-long throw at every boundary). The caller bridges it to the
* `compaction.input_truncated` trace frame (fidelity disclosure). Fired BEFORE the summary call, so
* it reports truncation even when the attempt later fails.
*/
onInputTruncated?: (info: {
label: "history" | "turn_prefix";
droppedChars: number;
keptChars: number;
}) => void;
/**
* The design/36 ToolSearch activation set LIVE at this pass (the
* runner threads `[...prepared.activeTools]` on all three lanes: boundary, PTL recovery, finish).
* Persisted into the landed compaction entry's details (`CompactionDetails.activeTools`) so the
* NEXT task boundary's re-derivation — whose transcript source (ToolSearch toolCall parts) this
* compaction just replaced with prose — re-seeds from a structured carrier instead (the
* checkpoint seed's `resume.seed.activeTools` sibling; prepare-task consumes it). Absent or
* empty ⇒ no key is written, byte-identical to pre-RB-402 entries.
*/
activeTools?: ReadonlyArray<string>;
}
/**
* Compact the session in place when context usage exceeds the threshold.
*
* Two call sites, same policy: the within-task `turn_boundary` hook (design/64 §25 (A) — after the
* session flush, before the next request's context rebuild) and `finish()` at task end. The caller
* must ensure no session write is in flight (both sites are between model requests, post-flush).
*
* THROWS (council §26.6 #6 contract note): a summarization failure — including a summary that came
* back EMPTY after the analysis-block strip (a real LLM call was burned, history left untouched) —
* is thrown, not swallowed; callers are expected to catch, count it toward the §17.4 breaker, and
* surface it via onError. A legitimate nothing-to-compact (under threshold/floor, no cut point)
* returns `{compacted:false}` quietly.
* Honors an optional cheaper `compactionModel` without mutating the harness's active model.
* Zero-config: uses DEFAULT_COMPACTION_SETTINGS unless overridden; settings pathological for the
* model's window are repaired via {@link sanitizeCompactionSettings}.
*/
export declare function maybeCompact(opts: MaybeCompactOptions): Promise<{
compacted: boolean;
/** design/134: set (with `compacted:false`) when a preCompact callback blocked this compaction
* (auto/manual triggers only — never on "forced"). NOT a failure: the caller must not count it
* toward the compaction breaker; the next boundary consults the callback again. */
blocked?: boolean;
/** MF-18 修①: set (with `compacted:false`) when compaction is DISABLED by settings
* (`enabled:false`) — the request can never be honored this run. Lets the caller resolve a manual
* /compact with the honest `"disabled"` outcome (and trace frame) instead of a structural noop. */
disabled?: boolean;
/** Set (with `compacted:false`) when the pass PASSED the trigger gate
* (natural or forced) but found nothing to summarize — no valid cut point / empty history. Was
* previously indistinguishable from the quiet under-threshold return, so a forced pass with
* nothing to do looked identical to one that never wanted to run; the caller's outcome event
* (`compaction_outcome{outcome:"noop"}`) reads this flag. */
noop?: true;
/** The id of the pass this result concludes — the value {@link MaybeCompactOptions.onStarted} was handed.
* Set exactly when `compacted` is true: every other return of this function ends before the pass commits,
* and a pass that commits and does not land THROWS (the caller learned the id from the start callback). */
pass?: string;
tokensBefore?: number;
/** The §25.2 STRUCTURAL pre/post delta in ONE coordinate — the only subtractable quantity here.
* Absent ⇒ the post measurement failed and no delta was taken. */
freedTokens?: number;
triggerTokens?: number;
/** P-13: post-compaction context size in the SAME coordinate as `triggerTokens` (estimate + anchor-less
* overhead). The caller's §25.2 regrowth floor = `postTriggerTokens × factor`, single-coordinate — never
* `triggerTokens − freedTokens` (which mixed a usage-anchored trigger with a chars/4 freed delta). */
postTriggerTokens?: number;
/** design/99 MF-18 `preserved_segment`: the session-tree entry id where the KEPT TAIL begins (the floor of
* the surviving history after this compaction). Surfaced so the `compacted` wire event can carry it (CC
* `SDKCompactBoundary.preserved_segment` parity); a consumer maps it to wire messages via its eventId↔entryId
* map (the same one service keeps for resumeAt). Set only when `compacted` is true. */
firstKeptEntryId?: string;
/** design/84 Seam C: true when this compaction reused a `summaryProvider` summary (no LLM call). */
reused?: boolean;
/** The working files re-read into the summary, in attachment order —
* surfaced so the `compacted` wire event carries them and a shell renders CC's post-compact
* `Read {path} (…)` cards. Set only when `compacted` is true and attachments were added. */
attachedFiles?: Array<{
path: string;
chars: number;
truncated: boolean;
}>;
/** design/123 D4: true when the NATURAL gate fired (est ≥ max(threshold, minTokens) — no force was
* needed); false on a forced landing (`force`/`forceUnderThreshold`). Set only when `compacted`. */
naturalTrigger?: boolean;
/** design/123 D4: whether the post-compaction size (`postTriggerTokens` coordinate) is STILL over the
* sanitized auto threshold — the "couldn't compact it down" signal {@link nextTrimForceBackoff}
* consumes. Set only when `compacted` and the post measurement succeeded. */
postOverThreshold?: boolean;
/** TB telemetry B4: wall time of the whole compaction pass (context build → summary
* call → history rewrite → post measurement). Set only when `compacted` — the `compacted` wire event
* carries it so a consumer sees what a boundary pause actually cost. */
durationMs?: number;
/** 压缩子阶段遥测 (1.283 批 G 观察项 / design/145 §6 正交后续批 — TB 观测到整段 ~55s 停顿,需要
* 子阶段读数支撑归因): wall-time split of THIS landed pass, in the same Date.now() coordinate as
* `durationMs`. `prepareMs` = context build → trigger math → cut-point selection → hooks/provider
* consult → clamp dry run + 门A/门B gating (everything before the summary call). `summaryMs` = the
* summary LLM leg (`compact()`: serialization → clamp → completion, incl. any PTL retries) — ABSENT
* on a provider-reuse pass (no LLM call happened; its provider consult time sits in `prepareMs`).
* `persistMs` = working-file attachments → session rewrite (`appendCompaction`) → post measurement →
* observers. The phases tile the pass exactly: prepareMs + (summaryMs ?? 0) + persistMs ===
* durationMs. `ptlRetries` = reactive prompt-too-long retries burned inside this pass's summary
* call(s) (both split-turn legs accumulate into the one counter; 0 on provider reuse). Set only when
* `compacted`. Exposure path note: runtask deliberately NOT touched this batch — the bridge to the
* `compaction.phase_timings` trace frame (trace.ts, defined-not-yet-emitted) is a follow-up wiring
* batch that adds an additive passthrough next to runtask's existing
* `...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {})` spread. */
phaseDurations?: CompactionPhaseDurations;
/** C3: set (with `compacted:false`) when the pass was over the auto threshold but the
* anti-thrash floor (`minTokens`) suppressed it — the caller bridges this to `compaction.suppressed`. */
suppressedByFloor?: {
estTokens: number;
floor: number;
};
/**
* Set (with `compacted:false`) when the trigger predicate could not be EVALUATED at all: the model
* declares no usable autocompact window (non-finite or <= 0), so `shouldCompact` refuses on the
* sentinel and `contextUsage` is honestly absent.
*
* This is a different state from "under the threshold", and it used to be indistinguishable from it:
* both returned a bare `{compacted:false}` with no `contextUsage`, no trace and no error, so a
* deployment whose model config lost its window had auto-compaction permanently disabled and the
* only notification was that nothing ever happened. The context still grows, the provider still
* eventually refuses the request, and the operator has nothing to read. The caller bridges this to
* a once-per-task host disclosure — silence is the defect here, so a noisy line is the fix.
*
* `estTokens` is the size this pass DID manage to estimate (structural + prompt overhead when no
* usage anchor is available), so the disclosure can say how big the context already is even though
* it cannot say how big the window is.
*/
unevaluableWindow?: {
estTokens: number;
/** WHICH of the three window fields the `??` precedence actually selected. A defined-but-unusable
* higher-precedence field (`autoCompactTokens: 0`) shadows a healthy lower one, so "no window is
* declared" is the wrong story on that path — the caller reports THIS field, not all three. */
windowField: "autoCompactTokens" | "contextTokens" | "contextWindow";
/** The unusable value that field carried, when it carried one at all (absent = nothing declared). */
windowValue?: number;
modelId?: string;
};
/**
* The trigger inputs THIS pass used, reported on
* EVERY return path — including the ordinary "nothing to do" one. A consumer rendering "how full is the
* context" was otherwise forced to re-derive the numerator itself, and a re-derivation that drifts from
* the engine's own is the projection-antipattern this repo keeps paying for: the displayed percentage
* would disagree with when compaction actually fires.
*
* `usedTokens` is the exact first argument to `shouldCompact` (usage-anchored when the anchor is fresh,
* structural + fixed prompt overhead otherwise — see the derivation above). `windowTokens` is the
* AUTOCOMPACT window (`autoCompactTokens` when the model declares one, else its context window), NOT
* necessarily the physical request window. `compactAtTokens` is the threshold those two are compared
* against, so `usedTokens > compactAtTokens` is precisely the engine's own trigger predicate.
*/
contextUsage?: {
usedTokens: number;
windowTokens: number;
compactAtTokens: number;
};
/** design/145 §3: this pass's summary call FELL BACK from the independent compaction model to the
* main model (window safety). Rides the `compacted` wire event as an additive field. */
modelFallback?: true;
/** design/145 §3: why the fallback happened — "window" is the only reason today. */
fallbackReason?: "window";
/** design/145 §3: the dry-run truncation ratio of a pass whose summary input WAS (or would be)
* clamp-truncated on the small model — set when a gate denied the fallback OR the ratio was
* within tolerance but non-zero. */
clampedRatio?: number;
/** design/145 §3: "budget" = the gate denied the fallback; "tolerance" = within-tolerance
* ordinary truncation (disclosed, not silent). */
clampReason?: "budget" | "tolerance";
}>;
/** What a landed compaction hands the wire frame. Structural, so both the full `maybeCompact` result
* and the settle lane's narrower `EndOfTaskCompaction` satisfy it without either importing the other. */
export interface CompactedFrameSource {
/** The pass this landing concludes (see {@link MaybeCompactOptions.onStarted}). */
pass?: string;
tokensBefore?: number;
freedTokens?: number;
postTriggerTokens?: number;
triggerTokens?: number;
durationMs?: number;
phaseDurations?: CompactionPhaseDurations;
firstKeptEntryId?: string;
attachedFiles?: Array<{
path: string;
chars: number;
truncated: boolean;
}>;
modelFallback?: true;
fallbackReason?: "window";
clampedRatio?: number;
clampReason?: "budget" | "tolerance";
/** Cross-session message frames that sat in the summarized prefix (present iff ≥ 1). */
summarizedCrossSessionMessages?: number;
}
/**
* THE mint of the `compacted` wire frame — one function, three lanes (the turn boundary, the
* prompt-too-long recovery, the end-of-task settle). The three used to carry a verbatim copy of the
* same fourteen-line literal, which is how a field reaches the wire on two lanes out of three: a key
* added to one copy is invisible in the others' diffs, and the only guard against it was a test that
* grepped the SOURCE TEXT of all three for a matching line. A behavior can be pinned; a copy can only
* be spelled.
*
* What differs per lane is exactly `trigger` and the identity envelope, so those are the parameters;
* everything else is derived here, once. Absence semantics live on the event type
* ({@link import("./task-event.js").TaskEvent}) and are the same whichever lane fires.
*/
export declare function mintCompactedEvent(comp: CompactedFrameSource, trigger: "auto" | "manual" | "forced", identity: TaskEventIdentity): Extract<TaskEvent, {
type: "compacted";
}>;
/**
* THE mint of the `compaction_started` wire frame — the opening half of the pair whose closing half is the
* `compacted` frame above (or a `compaction_outcome` that carries the same `pass`). One function, three
* lanes, as the landing's: the frame is the start report itself plus the identity envelope, so a lane that
* wants to announce a pass can only announce the one `maybeCompact` committed.
*/
export declare function mintCompactionStartedEvent(start: CompactionPassStart, identity: TaskEventIdentity): Extract<TaskEvent, {
type: "compaction_started";
}>;
/**
* One compaction pass's account on the wire: the `onStarted` seat to hand `maybeCompact`, and the id the frame
* that ENDS the pass must carry. A lane makes one per pass, hands `onStarted` to the pass, and spreads
* {@link CompactionPassReport.end} into every `compaction_outcome` the pass can end with after it committed — a
* pass that threw before it committed leaves it empty, which is exactly "no start was announced, so no `pass`
* names it". The landing needs no spread: `maybeCompact` returns the id and `mintCompactedEvent` reads it.
*/
export interface CompactionPassReport {
readonly onStarted: (start: CompactionPassStart) => void;
/** The announced pass's id; `undefined` until a start went on the wire. */
readonly pass: string | undefined;
/** `{ pass }` once a start was announced, `{}` before — for the frame that ends the pass. */
readonly end: {
pass?: string;
};
}
export declare function compactionPassReport(sink: {
push(event: TaskEvent): void;
}, ident: () => TaskEventIdentity): CompactionPassReport;
/**
* How many cross-session message frames the summarized prefix held: the engine-note user messages
* whose text is the drain's `<cross-session-message …>` envelope, before the kept tail begins. The
* frames were delivered (acked from the box) and are now only whatever the summary kept of them — the
* count is what lets a consumer say so; `0` is reported as absence.
*/
export declare function countSummarizedCrossSessionMessages(branch: ReadonlyArray<{
type: string;
id: string;
message?: {
role: string;
provenance?: string;
content: unknown;
};
}>, firstKeptEntryId: string): number;
/**
* design/123 D4 (r5) — trim-force BACKOFF transition (pure; the run-scoped bit lives in the caller).
* Spiral protection replacing the §25.2 floor's role on the trim-force path: trim pressure re-arms
* every request while the transcript is oversized, so an unconditional force would burn a summary
* call at every boundary when summaries CANNOT shrink the context (the LONGRUN-1b incompressible
* shape). The invariant: trim-force is permitted only while the LATEST landed compaction proved it
* can bring the context back under the threshold (or no compaction has landed yet). Transitions,
* evaluated after each maybeCompact result:
* 1. no compaction landed → state unchanged (a structural no-op proves nothing either way);
* 2. a landed compaction with `postOverThreshold` → "压不动": BACKOFF ON — compaction demonstrably
* cannot reach under-threshold, so forcing more of them only burns summary calls; the §25.2
* floor policy takes back over (natural triggers are never gated by this bit);
* 3. a landed compaction that posted UNDER the threshold → summaries compress again: BACKOFF OFF
* (this is how the natural floor-crossing compaction of design/123 D4's reset clause re-enables
* the trim-force — its healthy landing posts under threshold);
* 4. post measurement unavailable → state unchanged (never flip on missing evidence);
* 5. (COMPACTION-LIVE-AUDIT D-1, evaluated BEFORE rule 3) a TRIM-FORCED landing with
* `freedTokens ≤ 0` → negative-yield: BACKOFF ON. The trim layer deceives the usage-anchored
* estimate down into the sawtooth trough (live: est 18807 on a ~55k session), the forced pass
* finds little compressible history, and summary + working-file attachments make the transcript
* LARGER (live: 18807→44609, 12679→35214). `freedTokens` is the structural pre/post delta
* (clamped ≥0, so ≤0 means "freed nothing") — orthogonal to rule 2/3's threshold test, which
* misses this shape because the bloated post (44609) can still sit UNDER the threshold (44800),
* so rule 3 alone would re-enable the force and repeat the negative-yield pass every boundary.
* SEMA-ONLY DEFENSE, not a CC port — but the ORIGINAL justification for that label was wrong
* on both of its premises, and the corrected pair still supports it. (a) "CC's full compact
* keeps tail 0 and monotonically shrinks" describes the `GNo` branch, which a LOCAL threshold
* compaction never reaches (CC 2.1.223 `y9s` :432704 routes local passes to the reactive
* pipeline, which preserves the last group verbatim — `YMo` :399971, `s = 1` :399982); CC's
* default posture therefore has a keep-tail, exactly as ours does. (b) "CC has no
* request-layer trim" holds only for the MAIN conversation request: CC does own a
* keep-the-newest-groups-that-fit primitive (`UPb` :645223), whose only caller is the
* prompt-hook evaluator's own transcript window, not a main-lane request. What actually makes
* this regime ours is the third factor — the trim seam feeding a deceived usage anchor, and
* attachment mass re-entering after the fold — not the absence of a keep-tail. Scoped to trim-forced
* passes only (`trimForced`), so Seam C reused-summary landings (freed≈0 by design) on the
* natural/manual paths never trip it. Release path unchanged: backoff gates only the FORCE, a
* later natural landing that posts under threshold (rule 3, non-trim-forced) turns it back off.
*/
export declare function nextTrimForceBackoff(prev: boolean, comp: {
compacted: boolean;
postOverThreshold?: boolean;
freedTokens?: number;
naturalTrigger?: boolean;
}, trimForced?: boolean): boolean;
/** design/119 S3 (CC 198 rapid_refill_breaker): mutable per-task refill tracking state. */
export interface RapidRefillState {
lastCompactionTurn: number;
count: number;
}
/** Fresh tracking state (no compaction seen yet). */
export declare function createRapidRefillState(): RapidRefillState;
/**
* design/119 S3 (CC 2.1.198 rapid_refill_breaker, :229717/:476338): record that a compaction just
* happened at `turn` and report whether the thrash breaker should TRIP. A compaction landing within
* `minGapTurns` of the previous one is a "rapid refill"; `tripAt` consecutive rapid refills = the
* context is dominated by incompressible content and further compaction only burns summary calls.
* CC terminates the task; sema's caller (runtask) deviates conservatively — it opens the existing
* compaction breaker (no more compaction this task) and lets budget/PTL/turn limits end the run.
*/
export declare function recordCompactionAndCheckRapidRefill(state: RapidRefillState, turn: number, minGapTurns?: number, tripAt?: number): boolean;