/** * 任务状态卡 — the LEAD's request-side JIT state injection. * * The lead is the one role that has to hold the whole picture: which ledger * items are in flight, which are blocked, which child is still running, and * where the board is. Every one of those lives SOMEWHERE (host storage, the * child registry, the request layer's own note) but none of them rides the * request, so the model re-derives them by calling `tm_ledger` / `tm_join` * again — a round per look. This layer renders the state into ONE system * entry of ≤300 tokens, on the same `session.hook("context")` seam the request * layer already owns. * * The rules that make it safe: * * - **Only the lead, only our sessions.** A child session's agent is also one * of ours, so gating on scope alone would hand the card to a specialist; the * lead is the only role that holds the list. The scope gate is asked the * same way `v2-session.ts` asks it, and a foreign/unknown session is left * byte-exact (Team-scope isolation, #22). * - **One entry, replaced in place.** The card carries a `hash:` of its own * body as a VISIBLE marker; the gate itself compares BYTES (`prev === card`), * so "nothing changed" can never be claimed for two cards that merely share a * hash line. The same bytes mean NOTHING is written, different bytes rewrite * the SAME array slot (length and position do not move), and no marker entry * means one `push`. A host that replays the hook in-process therefore cannot * grow the card into a wall. * - **Empty state is not a card.** No open/doing/blocked ledger item and no * running child = the entry is omitted entirely, because an empty box costs * tokens and says nothing. * - **The window reading is its OWN entry** (`[Team 窗口占用]`), not a card line: * `窗口:% · 模型 ` with the percent rounded to the NEAREST 5% * multiple (42% → 40%), so a percent that jitters by a point or two does not * rewrite the request. The card's render and its byte gate never reference * it; an unreadable window (no usage, no catalog entry, a throwing read) * removes the entry rather than rendering a fabricated 0%. * - **A failed read says so, once per session.** A storage read that throws * injects nothing and writes one `state_skipped` trajectory row; a repeated * failure for the same session does not become a line per request. * - **Never throws into the hook.** A failure here allows the request through * and is counted, because a missing card is a missing extra. * - **The budget is paid for by trimming, not by lying.** The counts are the * real totals; the rendered list is trimmed from the OLDEST item until the * card fits `STATE_CARD_MAX_TOKENS`. Token cost reuses `estimateTokens` * from `src/tm/config.ts` (CJK-aware) so the number here and the number the * offloader uses can never disagree. */ import type { Ledger } from "../tm/ledger.js"; import { type UsagePercentReader } from "./v2-usage.js"; import type { TeamScope } from "./v2-scope.js"; import type { DynamicSink } from "./v2-tail.js"; import type { V2Context, V2Registration } from "./v2-types.js"; /** 卡片标记:`event.system` 里以它开头的条目就是状态卡(唯一一条)。 */ export declare const STATE_CARD_MARKER = "[Team \u72B6\u6001\u5361]"; /** 窗口占用条目的标记——它是**独立条目**,不参与状态卡的渲染与 hash 门控。 */ export declare const WINDOW_MARKER = "[Team \u7A97\u53E3\u5360\u7528]"; /** 窗口占用的分桶步长:最近 5% 的整数倍(42% → 40%)。 */ export declare const WINDOW_BUCKET_STEP = 5; /** 卡片 token 上限。 超过就按「最旧账本项」开始裁剪。 */ export declare const STATE_CARD_MAX_TOKENS = 300; /** 每条账本项渲染的字符上限。 */ export declare const STATE_CARD_ITEM_CHARS = 60; /** 看板根的字符上限(超长工作区路径不该吃满预算)。 */ export declare const STATE_CARD_BOARD_CHARS = 120; /** 子代理的最小形状——`tmRuntime.hostChildren()` 的超集即可。 */ export interface StateCardChild { sessionID: string; parentSessionID?: string; agent: string; state: string; } export interface StateCardRenderInput { ledger: Ledger | null; children: StateCardChild[]; boardRoot: string; /** 距上一张卡片以来卸载的消息数;0 / 缺省时整行省略。 */ pruneDelta?: number; /** * 只算这个会话的子代理。 缺省 / 空 = 不渲染 children 行(见 `runningChildren`), * 绝不退化成「列全部」——那是别人的会话。 */ sessionID?: string; } export interface StateCardInput { enabled?: boolean; boardRoot: string; scope?: TeamScope; loadLedger: (sessionID: string) => Promise; children: () => StateCardChild[]; /** 累计离载条数(读 prune 报告的 `offloaded`)。 只取差量,所以「上轮」是可信的。 */ pruneCount?: () => number; /** * 窗口占用的读取器;缺省时由 `createUsagePercent(ctx)` 从 `ctx.model.list()` + * 消息自带的 usage 读。注入点只为单测准备,生产路径没有第二个分母来源。 */ usageOf?: UsagePercentReader; /** * 易变注入的尾部通道 (`dynamicTail: "on"`). When handed in, the card and the * window entry go to the request's TAIL instead of `event.system`, and the * hash/bucket gate becomes a same-request idempotency check (`peek`) rather * than a cross-request byte gate — the sink is drained every request, so * there is nothing from the previous turn to compare against. */ sink?: DynamicSink; onEvent?: (row: Record) => void; } export interface StateCardReport { enabled: boolean; /** 新增了一条状态卡的请求数。 */ injected: number; /** 同一条条目被原地替换的请求数。 */ replaced: number; /** hash 相同、一个字节都没动的请求数。 */ unchanged: number; /** 空状态(无进行/阻塞/待办项且无运行子代理)。 */ empty: number; /** 状态已空、但上一次注入的卡还在请求里 —— 删掉它的请求数(不留高估形态)。 */ cleared: number; /** 存储读取抛错、因此不注入的请求数。 */ skipped: number; /** 不是 lead 的请求数。 */ agentSkipped: number; /** 是 lead 但不属于我们治理范围(scope 非 ours)的请求数。 */ foreignSkipped: number; /** hook 内部抛错(永不外溢,只计数)。 */ threw: number; /** * 窗口占用条目(独立于状态卡): * - `windowInjected`:本轮请求里出现了这条条目(新推入**或**原地替换同一槽位); * - `windowUnchanged`:分桶与模型都没变,一个字节都没写; * - `windowCleared`:读不到窗口/用量,已存在的那条被摘掉; * - `windowAbsent`:读不到,而且本来就没有这条条目。 */ windowInjected: number; windowUnchanged: number; windowCleared: number; windowAbsent: number; lastWhy: string; } export interface StateCardLayer { registrations: V2Registration[]; report: StateCardReport; } /** 16 位 hex 的状态指纹。 */ export declare function stateCardHash(body: string): string; /** * 窗口占用的分桶:最近 5% 的整数倍。 分桶是为了让「同一个档位」在两次请求之间 * 逐字节相等——百分比每次都可能抖一两位,而「窗口还剩多少」这个判断不需要那么细。 */ export declare function windowBucket(percent: number): number; /** * 窗口占用条目。 它是**独立**的一条 system 条目(自己的 marker、自己的 hash), * 状态卡的渲染与字节门控完全不引用它——两件事各自可靠。 */ export declare function renderWindowEntry(bucket: number, model: string): string; /** * 渲染一张卡片,或返回 null(空状态 = 不注入)。 纯函数:同样的状态必然 * 产出逐字节相同的文本,这是字节门控能成立的前提。 */ export declare function renderStateCard(input: StateCardRenderInput): string | null; /** * 注册到 `session.hook("context")` 的状态卡层。 依赖全部注入,所以测试可以 * 用一个内存账本驱动它,而生产路径用的是 `tm_ledger` 的那个 store 实例。 */ export declare function applyV2StateCard(ctx: V2Context, input: StateCardInput): Promise; //# sourceMappingURL=v2-state.d.ts.map