blob: 98be3a653402dbe3b8bbc2716c01d29c264a0789 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { SessionEvent } from '@maka/core/events';
/**
* How full the context is right now, re-asked mid-turn (#4545).
*
* The statusline ctx segment historically moved once per turn, when the
* end-of-turn `token_usage` event landed — a long agentic turn burned context
* for minutes with the indicator frozen at the previous turn's value. The Host
* already commits a latest-context snapshot at every settled provider request
* (the desktop inspector's data source), so the TUI pulls it on the same
* signal desktop uses instead of growing a parallel push event.
*/
/**
* When a live session's context snapshot is worth re-reading. Mirrors the
* desktop inspector's TRACE_RELEVANT_EVENT_TYPES
* (apps/desktop/src/renderer/session-trace-refresh.ts): the snapshot is
* committed at each settled provider request, so the events that follow one —
* tool boundaries above all — are the moments the answer can have changed.
* That is deliberately not "every event": a streaming turn emits text deltas
* continuously, and none of them moves the snapshot.
*/
const CTX_REFRESH_EVENT_TYPES: ReadonlySet<SessionEvent['type']> = new Set([
'tool_start',
'tool_result',
'token_usage',
'provider_retry',
'error',
'complete',
'abort',
]);
export function isCtxRefreshRelevantEvent(event: SessionEvent): boolean {
return CTX_REFRESH_EVENT_TYPES.has(event.type);
}
/**
* Long enough to absorb a step boundary's event burst, short enough to feel
* live. Same value and rationale as the desktop inspector's
* TRACE_REFRESH_DEBOUNCE_MS.
*/
export const CTX_REFRESH_DEBOUNCE_MS = 400;
type CancelScheduled = () => void;
export interface CtxRefresher {
/** Records an event; schedules a query when the event can have moved the snapshot. */
observe(event: SessionEvent): void;
/** Drops a scheduled query and retires any in-flight one. */
cancel(): void;
}
/**
* Coalesces a burst of refresh-worthy events into one query, and lets only
* the latest issued query apply. A query that resolves after a newer one was
* issued — or after `cancel` retired it — is dropped: the answer a slow read
* brings back describes an older snapshot than the one already shown, and a
* torn-down session is not owed an update at all. A failed query leaves the
* last value standing: it is still the newest answer anyone has.
*
* The scheduler is injected so the policy is testable without a wall clock,
* and follows the CLI's ticker convention: schedule returns the cancel.
*/
export function createCtxRefresher<T>(input: {
query: () => Promise<T>;
apply: (result: T) => void;
delayMs: number;
schedule: (callback: () => void, delayMs: number) => CancelScheduled;
}): CtxRefresher {
let cancelScheduled: CancelScheduled | undefined;
let revision = 0;
const dropScheduled = (): void => {
cancelScheduled?.();
cancelScheduled = undefined;
};
const run = (): void => {
cancelScheduled = undefined;
const requestRevision = ++revision;
void input.query().then(
(result) => {
if (requestRevision !== revision) return;
input.apply(result);
},
() => {},
);
};
return {
observe(event) {
if (!isCtxRefreshRelevantEvent(event)) return;
// Restart rather than stack: the last event of a burst is the one whose
// snapshot the reader wants, and an earlier timer would read before it.
dropScheduled();
cancelScheduled = input.schedule(run, input.delayMs);
},
cancel() {
dropScheduled();
revision += 1;
},
};
}
/** Default one-shot timer; unref'd so a pending refresh never holds the CLI open. */
export function scheduleCtxRefreshTimeout(callback: () => void, delayMs: number): CancelScheduled {
const handle = setTimeout(callback, delayMs);
handle.unref();
return () => clearTimeout(handle);
}