Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@

### Fixed

- The composer's context gauge now says so instead of holding the pre-compaction
figure after a fold. A compaction replaces the prompt the last measured request
described, and no provider has tokenized the replacement yet, so the newest real
count is stale rather than current — showing it read as a live measurement of
what the session was about to send. The gauge walks the transcript backwards and
whichever fact comes first decides: a `context_compacted` note newer than every
measurement renders `?` with a tooltip saying why, and a measurement newer than
the note stands. The live per-settled-request snapshot still wins when it landed
after the boundary, so a mid-turn fold recovers as soon as the next step settles
rather than waiting for the turn to end. A failed-open fold is not a boundary:
that request went out with its full raw history.
- Fixed a renderer crash dialog reporting React error #185 ("Maximum update depth
exceeded") coming from the composer's prompt-history inline completion (#4117): the
offer engine the 0.1.11 composer fed could flip-flop its announcement state on
Expand Down
195 changes: 174 additions & 21 deletions apps/desktop/src/main/__tests__/latest-request-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,36 @@

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { selectLatestRequestUsage } from '../../renderer/chat-composer-region.js';
import {
resolveContextUsage,
selectLatestRequestUsage,
} from '../../renderer/application/contracts/session-inspector/latest-request-usage.js';

const ROUTE = { llmConnectionId: 'conn-a' };
const MODEL = 'model-a';

function usage(anchor?: {
inputTokens: number;
outputTokens?: number;
modelId?: string;
connectionId?: string;
}) {
return { type: 'token_usage', ...(anchor ? { lastRequestAnchor: anchor } : {}) };
function usage(
anchor?: {
inputTokens: number;
outputTokens?: number;
modelId?: string;
connectionId?: string;
},
ts?: number,
) {
return {
type: 'token_usage',
...(ts !== undefined ? { ts } : {}),
...(anchor ? { lastRequestAnchor: anchor } : {}),
};
}

function compactionNote(kind: string, ts?: number) {
return { type: 'system_note', kind, ...(ts !== undefined ? { ts } : {}) };
}

test('reads the newest anchor on the active route', () => {
const tokens = selectLatestRequestUsage(
const reading = selectLatestRequestUsage(
[
usage({ inputTokens: 10, outputTokens: 2, modelId: MODEL, connectionId: 'conn-a' }),
{ type: 'assistant' },
Expand All @@ -43,52 +57,100 @@ test('reads the newest anchor on the active route', () => {
MODEL,
ROUTE,
);
assert.equal(tokens, 120);
assert.deepEqual(reading, { kind: 'tokens', tokens: 120 });
});

test('scans past an anchorless usage row, which is what manual compaction writes', () => {
// `/compact` appends a synthetic `token_usage` with no anchor. The runtime's
// own reader skips it and keeps the last real request; stopping there would
// blank the indicator after every manual compaction.
const tokens = selectLatestRequestUsage(
// read the fold's own record as a count of zero.
const reading = selectLatestRequestUsage(
[
usage({ inputTokens: 100, outputTokens: 20, modelId: MODEL, connectionId: 'conn-a' }),
usage(),
],
MODEL,
ROUTE,
);
assert.equal(tokens, 120);
assert.deepEqual(reading, { kind: 'tokens', tokens: 120 });
});

test('a compaction boundary newer than every measurement supersedes it', () => {
// The fold replaced the prompt the newest count described, and nothing has
// measured the replacement. The stale figure must not be shown as a live
// reading of what the session is about to send.
const reading = selectLatestRequestUsage(
[
usage({ inputTokens: 100, outputTokens: 20, modelId: MODEL, connectionId: 'conn-a' }, 1_000),
compactionNote('context_compacted', 2_000),
usage(undefined, 2_100),
],
MODEL,
ROUTE,
);
assert.deepEqual(reading, { kind: 'compacted', at: 2_000 });
});

test('a measurement newer than the boundary stands, which is the post-fold reading', () => {
const reading = selectLatestRequestUsage(
[
usage({ inputTokens: 100, outputTokens: 20, modelId: MODEL, connectionId: 'conn-a' }, 1_000),
compactionNote('context_compacted', 2_000),
usage({ inputTokens: 30, outputTokens: 5, modelId: MODEL, connectionId: 'conn-a' }, 3_000),
],
MODEL,
ROUTE,
);
assert.deepEqual(reading, { kind: 'tokens', tokens: 35 });
});

test('a failed-open fold is not a boundary', () => {
// The fold was refused and the request went out with its full raw history, so
// the measurement behind the note still describes what was sent.
const reading = selectLatestRequestUsage(
[
usage({ inputTokens: 100, outputTokens: 20, modelId: MODEL, connectionId: 'conn-a' }),
compactionNote('context_compaction_failed_open'),
],
MODEL,
ROUTE,
);
assert.deepEqual(reading, { kind: 'tokens', tokens: 120 });
});

test('a boundary with no measurement behind it is still a superseded reading', () => {
const reading = selectLatestRequestUsage([compactionNote('context_compacted')], MODEL, ROUTE);
assert.deepEqual(reading, { kind: 'compacted' });
});

test('refuses an anchor from another model', () => {
// A token count is a number in one model's tokenizer. Pairing model A's
// count with model B's window produces a precise-looking figure about a
// request the user is not making.
const tokens = selectLatestRequestUsage(
const reading = selectLatestRequestUsage(
[usage({ inputTokens: 100_000, modelId: 'model-b', connectionId: 'conn-a' })],
MODEL,
ROUTE,
);
assert.equal(tokens, undefined);
assert.equal(reading, undefined);
});

test('refuses an anchor from another connection', () => {
const tokens = selectLatestRequestUsage(
const reading = selectLatestRequestUsage(
[usage({ inputTokens: 100, modelId: MODEL, connectionId: 'conn-b' })],
MODEL,
ROUTE,
);
assert.equal(tokens, undefined);
assert.equal(reading, undefined);
});

test('refuses an anchor written before anchors carried their route', () => {
const tokens = selectLatestRequestUsage(
const reading = selectLatestRequestUsage(
[usage({ inputTokens: 100, outputTokens: 20 })],
MODEL,
ROUTE,
);
assert.equal(tokens, undefined);
assert.equal(reading, undefined);
});

test('refuses when there is no active route yet', () => {
Expand All @@ -98,10 +160,101 @@ test('refuses when there is no active route yet', () => {
});

test('refuses a non-positive input count', () => {
const tokens = selectLatestRequestUsage(
const reading = selectLatestRequestUsage(
[usage({ inputTokens: 0, modelId: MODEL, connectionId: 'conn-a' })],
MODEL,
ROUTE,
);
assert.equal(tokens, undefined);
assert.equal(reading, undefined);
});

test('the snapshot is the reading when it is the newer answer', () => {
assert.deepEqual(
resolveContextUsage({
latestRequestUsage: { kind: 'tokens', tokens: 120 },
live: { usageTokens: 130, completedAt: 1_500 },
}),
{ kind: 'measured', tokens: 130 },
);
// No snapshot at all leaves the anchor standing.
assert.deepEqual(
resolveContextUsage({ latestRequestUsage: { kind: 'tokens', tokens: 120 } }),
{ kind: 'measured', tokens: 120 },
);
// The snapshot can still vouch when the transcript established nothing.
assert.deepEqual(
resolveContextUsage({ latestRequestUsage: undefined, live: { usageTokens: 130 } }),
{ kind: 'measured', tokens: 130 },
);
assert.deepEqual(resolveContextUsage({ latestRequestUsage: undefined }), {
kind: 'unavailable',
});
});

test('a boundary supersedes the snapshot it landed after', () => {
// The manual `/compact` case: the snapshot still describes the pre-fold
// prompt, so the gauge says unknown rather than holding that figure.
assert.deepEqual(
resolveContextUsage({
latestRequestUsage: { kind: 'compacted', at: 2_000 },
live: { usageTokens: 90_000, completedAt: 1_000 },
}),
{ kind: 'stale', reason: 'compaction' },
);
assert.deepEqual(
resolveContextUsage({ latestRequestUsage: { kind: 'compacted', at: 2_000 } }),
{ kind: 'stale', reason: 'compaction' },
);
// An untimed snapshot cannot be shown to be newer than a boundary, and a
// guess in that position is the precise-looking lie this state exists to
// refuse.
assert.deepEqual(
resolveContextUsage({
latestRequestUsage: { kind: 'compacted', at: 2_000 },
live: { usageTokens: 90_000 },
}),
{ kind: 'stale', reason: 'compaction' },
);
});

test('a snapshot newer than the boundary is the post-fold reading', () => {
// A mid-turn fold is followed by steps that really do measure the smaller
// prompt, so the gauge recovers without waiting for the turn to end.
assert.deepEqual(
resolveContextUsage({
latestRequestUsage: { kind: 'compacted', at: 2_000 },
live: { usageTokens: 30_000, completedAt: 2_500 },
}),
{ kind: 'measured', tokens: 30_000 },
);
});


test('a selected live measurement carries only its own metered window', () => {
assert.deepEqual(
resolveContextUsage({
latestRequestUsage: { kind: 'tokens', tokens: 120 },
live: { usageTokens: 130, contextWindow: 1_000, completedAt: 1_500 },
}),
{ kind: 'measured', tokens: 130, meteredWindow: 1_000 },
);
assert.deepEqual(
resolveContextUsage({
latestRequestUsage: { kind: 'compacted', at: 2_000 },
live: { usageTokens: 130, contextWindow: 1_000, completedAt: 1_500 },
}),
{ kind: 'stale', reason: 'compaction' },
);
});

test('equal or missing boundary times cannot establish a post-fold measurement', () => {
for (const at of [undefined, 2_000]) {
assert.deepEqual(
resolveContextUsage({
latestRequestUsage: { kind: 'compacted', at },
live: { usageTokens: 130, completedAt: 2_000 },
}),
{ kind: 'stale', reason: 'compaction' },
);
}
});
25 changes: 13 additions & 12 deletions apps/desktop/src/main/__tests__/live-context-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ function scriptedQuery() {
describe('liveContextUsageFromDiagnostics', () => {
it('maps a matching snapshot onto the gauge, window included', () => {
assert.deepEqual(liveContextUsageFromDiagnostics(available(), ROUTE), {
completedAt: 1,
usageTokens: 79_436,
contextWindow: 128_000,
});
Expand Down Expand Up @@ -113,7 +114,7 @@ describe('liveContextUsageFromDiagnostics', () => {
it('stands alone without a window', () => {
assert.deepEqual(
liveContextUsageFromDiagnostics(available({ contextWindow: undefined }), ROUTE),
{ usageTokens: 79_436 },
{ completedAt: 1, usageTokens: 79_436 },
);
});
});
Expand All @@ -136,7 +137,7 @@ describe('createLiveContextUsageTracker', () => {
await Promise.resolve();
// The leading `undefined` is the aim itself: whatever stood on screen
// before cannot answer for this target, so it clears before the read.
assert.deepEqual(seen, [undefined, { usageTokens: 79_436, contextWindow: 128_000 }]);
assert.deepEqual(seen, [undefined, { completedAt: 1, usageTokens: 79_436, contextWindow: 128_000 }]);
tracker.dispose();
});

Expand Down Expand Up @@ -182,12 +183,12 @@ describe('createLiveContextUsageTracker', () => {
assert.equal(query.pending.length, 1);
timer.fire();
assert.equal(query.pending.length, 2);
query.pending[1]!.resolve(available({ inputTokens: 52_000 }));
query.pending[1]!.resolve(available({ inputTokens: 52_000, completedAt: 2 }));
await Promise.resolve();
assert.deepEqual(seen, [
undefined,
{ usageTokens: 40_000, contextWindow: 128_000 },
{ usageTokens: 52_000, contextWindow: 128_000 },
{ completedAt: 1, usageTokens: 40_000, contextWindow: 128_000 },
{ completedAt: 2, usageTokens: 52_000, contextWindow: 128_000 },
]);
tracker.dispose();
});
Expand Down Expand Up @@ -231,7 +232,7 @@ describe('createLiveContextUsageTracker', () => {
await Promise.resolve();
query.pending[0]!.resolve(available({ inputTokens: 10_000 }));
await Promise.resolve();
assert.deepEqual(seen, [undefined, { usageTokens: 60_000, contextWindow: 128_000 }]);
assert.deepEqual(seen, [undefined, { completedAt: 1, usageTokens: 60_000, contextWindow: 128_000 }]);
tracker.dispose();
});

Expand All @@ -254,7 +255,7 @@ describe('createLiveContextUsageTracker', () => {
query.pending[1]!.reject(new Error('host not ready'));
await Promise.resolve();
await Promise.resolve();
assert.deepEqual(seen, [undefined, { usageTokens: 79_436, contextWindow: 128_000 }]);
assert.deepEqual(seen, [undefined, { completedAt: 1, usageTokens: 79_436, contextWindow: 128_000 }]);
tracker.dispose();
});

Expand All @@ -276,14 +277,14 @@ describe('createLiveContextUsageTracker', () => {
// Switching sessions makes the standing number unanswerable: it must
// leave the screen BEFORE the new target's first read lands…
tracker.setTarget({ sessionId: 's2', route: ROUTE });
assert.deepEqual(seen, [undefined, { usageTokens: 79_436, contextWindow: 128_000 }, undefined]);
assert.deepEqual(seen, [undefined, { completedAt: 1, usageTokens: 79_436, contextWindow: 128_000 }, undefined]);

// …and a rejected first read on the new target keeps it cleared, rather
// than pinning the previous session's number in place indefinitely.
query.pending[1]!.reject(new Error('host not ready'));
await Promise.resolve();
await Promise.resolve();
assert.deepEqual(seen, [undefined, { usageTokens: 79_436, contextWindow: 128_000 }, undefined]);
assert.deepEqual(seen, [undefined, { completedAt: 1, usageTokens: 79_436, contextWindow: 128_000 }, undefined]);
tracker.dispose();
});

Expand All @@ -310,7 +311,7 @@ describe('createLiveContextUsageTracker', () => {
query.pending[1]!.reject(new Error('host not ready'));
await Promise.resolve();
await Promise.resolve();
assert.deepEqual(seen, [undefined, { usageTokens: 79_436, contextWindow: 128_000 }]);
assert.deepEqual(seen, [undefined, { completedAt: 1, usageTokens: 79_436, contextWindow: 128_000 }]);
tracker.dispose();
});

Expand Down Expand Up @@ -355,7 +356,7 @@ describe('createLiveContextUsageTracker', () => {
await Promise.resolve();
// Aiming, then leaving s1 clears its (never-landed) reading, then s2's
// lands; the stale s1 read resolving late must not overwrite it.
assert.deepEqual(seen, [undefined, undefined, { usageTokens: 5_000, contextWindow: 128_000 }]);
assert.deepEqual(seen, [undefined, undefined, { completedAt: 1, usageTokens: 5_000, contextWindow: 128_000 }]);
tracker.dispose();
});

Expand All @@ -381,7 +382,7 @@ describe('createLiveContextUsageTracker', () => {
await Promise.resolve();
assert.deepEqual(seen, [
undefined,
{ usageTokens: 79_436, contextWindow: 128_000 },
{ completedAt: 1, usageTokens: 79_436, contextWindow: 128_000 },
undefined,
undefined,
]);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2469,7 +2469,7 @@ function AppShellContent({
activeModel={activeModel}
activeModelLabel={activeModelLabel}
activeProviderType={activeConnection?.providerType}
latestRequestUsageTokens={selectLatestRequestUsage(messages, activeModel, activeSessionForModelControls)}
latestRequestUsage={selectLatestRequestUsage(messages, activeModel, activeSessionForModelControls)}
onOpenContextUsage={() => commands.toggleTool('inspector')}
LiveContextUsageProbe={LiveContextUsageProbe}
contextUsageSessionId={ownerActiveId}
Expand Down
Loading
Loading