@cortexkit/opencode-magic-context 0.37.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/{announcement-6c1g4es4.js → announcement-5eg4t64s.js} +6 -6
- package/dist/config/schema/magic-context.d.ts +1 -1
- package/dist/{conflict-warning-hook-an4fxhwf.js → conflict-warning-hook-f406ggbw.js} +5 -5
- package/dist/{derive-budgets-0f8xpyw2.js → derive-budgets-xk8cy5e6.js} +5 -5
- package/dist/{emscripten-module-ap8t3st0.js → emscripten-module-49k43zfy.js} +1 -1
- package/dist/{emscripten-module-682pfzaa.js → emscripten-module-ex4v0x4z.js} +1 -1
- package/dist/{emscripten-module-r5h7aghp.js → emscripten-module-jfs3d1dq.js} +1 -1
- package/dist/{emscripten-module-bkktz927.js → emscripten-module-ss4g4spn.js} +1 -1
- package/dist/features/magic-context/dreamer/classify-prompt.d.ts +5 -1
- package/dist/features/magic-context/dreamer/classify-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/manifest-parser.d.ts +8 -0
- package/dist/features/magic-context/dreamer/manifest-parser.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +7 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts +18 -0
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/retrospective-orphan-sweep.d.ts +17 -6
- package/dist/features/magic-context/dreamer/retrospective-orphan-sweep.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +5 -0
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -1
- package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/fail-closed-block.d.ts +6 -1
- package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
- package/dist/features/magic-context/memory/types.d.ts +16 -0
- package/dist/features/magic-context/memory/types.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +3 -2
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/reclaim-protection.d.ts +13 -0
- package/dist/features/magic-context/reclaim-protection.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts +7 -2
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-embedding-measurements.d.ts +1 -1
- package/dist/features/magic-context/storage-embedding-measurements.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +7 -6
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-session-tables.d.ts +18 -0
- package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -0
- package/dist/features/magic-context/storage-tags.d.ts +13 -5
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +22 -0
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/{ffi-d8pnn3mn.js → ffi-bbm9vkfa.js} +1 -1
- package/dist/{ffi-04jzypzk.js → ffi-kb5r0x1j.js} +1 -1
- package/dist/{ffi-t8b862vx.js → ffi-qnrt0y78.js} +1 -1
- package/dist/{ffi-9wvt2wwz.js → ffi-yx2hpf4p.js} +1 -1
- package/dist/hooks/auto-update-checker/checker.d.ts +8 -1
- package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-cycle.d.ts +16 -0
- package/dist/hooks/magic-context/channel2-cycle.d.ts.map +1 -0
- package/dist/hooks/magic-context/channel2-delivery.d.ts +3 -9
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +2 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +33 -103
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
- package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
- package/dist/hooks/magic-context/empty-task-output.d.ts +16 -0
- package/dist/hooks/magic-context/empty-task-output.d.ts.map +1 -0
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts +2 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/fold-execution-gate.d.ts +2 -0
- package/dist/hooks/magic-context/fold-execution-gate.d.ts.map +1 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +6 -0
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/live-session-state.d.ts +3 -0
- package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +4 -2
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/strip-content.d.ts +10 -8
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +78 -0
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -0
- package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +7 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-cae89eas.js → index-1h9t9xb5.js} +2 -2
- package/dist/{index-wckvcay0.js → index-1yh8g550.js} +8 -6
- package/dist/index-22mw8km4.js +558 -0
- package/dist/{index-cgyfn1s2.js → index-2wchnyk6.js} +4 -2
- package/dist/{index-0ze9p983.js → index-5znjkjws.js} +9996 -7043
- package/dist/{index-4cz5nfxf.js → index-btj2qhmc.js} +5 -5
- package/dist/{index-q0t189ay.js → index-mg1yx1ph.js} +28 -36
- package/dist/{index-ayd60xnw.js → index-rgdqj8nj.js} +3 -3
- package/dist/{index-ygcng9t8.js → index-rpgg35ms.js} +50 -50
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +776 -846
- package/dist/{module-ES6BEMUI-atfbtnbp.js → module-ES6BEMUI-1yjdz59c.js} +4 -4
- package/dist/{module-asyncify-2EFITU5U-7xckhw8w.js → module-asyncify-2EFITU5U-fdkx4fhy.js} +1 -1
- package/dist/plugin/dream-timer.d.ts +5 -0
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +2 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{prompt-context-jpe2dstn.js → prompt-context-29qkb28f.js} +1 -1
- package/dist/{recomp-orchestrator-c3ff16k7.js → recomp-orchestrator-p030kxtc.js} +15 -15
- package/dist/{resolve-fallbacks-5n5tvge1.js → resolve-fallbacks-qf0ekmz9.js} +3 -3
- package/dist/{rpc-notifications-4d22ybn5.js → rpc-notifications-1ffm755w.js} +6 -6
- package/dist/{safe-notification-target-b693f9x3.js → safe-notification-target-tepzc2xa.js} +3 -3
- package/dist/{send-session-notification-dxske5zm.js → send-session-notification-6v5b72b4.js} +7 -7
- package/dist/shared/harness-provider-map.d.ts +6 -0
- package/dist/shared/harness-provider-map.d.ts.map +1 -1
- package/dist/shared/models-dev-cache.d.ts +2 -0
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +15 -0
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +9 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/tail-hygiene-status.d.ts +15 -0
- package/dist/shared/tail-hygiene-status.d.ts.map +1 -0
- package/dist/shared/window-geometry.d.ts +3 -2
- package/dist/shared/window-geometry.d.ts.map +1 -1
- package/dist/{storage-meta-session-mk4gsnke.js → storage-meta-session-2306t3j7.js} +8 -8
- package/dist/{task-config-gbp7r23g.js → task-config-at3adc59.js} +6 -6
- package/package.json +4 -2
- package/src/config/schema/magic-context.test.ts +1 -0
- package/src/config/schema/magic-context.ts +5 -5
- package/src/shared/harness-provider-map.test.ts +36 -0
- package/src/shared/harness-provider-map.ts +8 -0
- package/src/shared/models-dev-cache.test.ts +25 -0
- package/src/shared/models-dev-cache.ts +5 -4
- package/src/shared/rpc-server.ts +1 -0
- package/src/shared/rpc-types.ts +16 -0
- package/src/shared/rpc-utils.test.ts +25 -0
- package/src/shared/rpc-utils.ts +80 -0
- package/src/shared/tail-hygiene-status.ts +57 -0
- package/src/shared/window-geometry.test.ts +34 -1
- package/src/shared/window-geometry.ts +37 -40
- package/src/tui/index.tsx +12 -2
- package/src/tui/slots/sidebar-content.tsx +17 -5
- package/src/tui/tui-compiled-runtime-imports.test.ts +3 -0
- package/src/tui-compiled/index.tsx +312 -290
- package/src/tui-compiled/slots/sidebar-content.tsx +316 -287
- package/dist/index-crke4z5j.js +0 -3022
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* their exported functions remain distinct deliberately. A future OMP catalog
|
|
17
17
|
* rename must not silently change plain-Pi behavior (or vice versa).
|
|
18
18
|
*
|
|
19
|
+
* The OpenCode Zen gateway is the one place the two harnesses diverge: OpenCode
|
|
20
|
+
* and plain Pi both name it `opencode`, while OMP exposes the same gateway as
|
|
21
|
+
* `opencode-zen`. Only the OMP map carries that pair; plain Pi keeps `opencode`
|
|
22
|
+
* as an identity. `opencode-go` is a distinct gateway (the "go" variant at
|
|
23
|
+
* opencode.ai/zen/go) and stays unmapped on both harnesses.
|
|
24
|
+
*
|
|
19
25
|
* The mapping is intentionally not a one-to-one provider identity. Both
|
|
20
26
|
* harnesses also expose plain `openai` and `google` providers for direct API
|
|
21
27
|
* keys, while the canonical prefix does not record whether a subscription or
|
|
@@ -43,11 +49,13 @@ const PI_TO_CANONICAL_PROVIDER: Readonly<Record<string, string>> = {
|
|
|
43
49
|
const CANONICAL_TO_OMP_PROVIDER: Readonly<Record<string, string>> = {
|
|
44
50
|
openai: "openai-codex",
|
|
45
51
|
google: "google-antigravity",
|
|
52
|
+
opencode: "opencode-zen",
|
|
46
53
|
};
|
|
47
54
|
|
|
48
55
|
const OMP_TO_CANONICAL_PROVIDER: Readonly<Record<string, string>> = {
|
|
49
56
|
"openai-codex": "openai",
|
|
50
57
|
"google-antigravity": "google",
|
|
58
|
+
"opencode-zen": "opencode",
|
|
51
59
|
};
|
|
52
60
|
|
|
53
61
|
/** Remap only the provider prefix (text before the first "/"), preserving the
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getModelsDevCacheState,
|
|
8
8
|
getSdkContextLimit,
|
|
9
9
|
getSdkInputLimit,
|
|
10
|
+
getSdkWindowGeometry,
|
|
10
11
|
refreshModelLimitsAfterAuthOnce,
|
|
11
12
|
refreshModelLimitsFromApi,
|
|
12
13
|
resetAuthRewarmLatchForTest,
|
|
@@ -148,6 +149,30 @@ describe("models-dev-cache (SDK-only)", () => {
|
|
|
148
149
|
clearModelsDevCache();
|
|
149
150
|
});
|
|
150
151
|
|
|
152
|
+
test("honors the reporter's default output_reserve on the SDK geometry path", async () => {
|
|
153
|
+
await refreshModelLimitsFromApi(
|
|
154
|
+
makeClient([
|
|
155
|
+
{
|
|
156
|
+
id: "openai-codex",
|
|
157
|
+
models: {
|
|
158
|
+
"gpt-5.6-sol": {
|
|
159
|
+
limit: { context: 400_000, input: 272_000, output: 128_000 },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
]),
|
|
164
|
+
);
|
|
165
|
+
setOutputReserveConfig({ default: 16_384 });
|
|
166
|
+
|
|
167
|
+
const geometry = getSdkWindowGeometry("openai-codex", "gpt-5.6-sol");
|
|
168
|
+
expect(geometry?.usableSoft).toBe(272_000 - 16_384);
|
|
169
|
+
expect(geometry?.derivation).toMatchObject({
|
|
170
|
+
window: 272_000,
|
|
171
|
+
reserve: 16_384,
|
|
172
|
+
reserveSource: "output_config",
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
151
176
|
test("resolves from the SDK and prefers limit.input over limit.context", async () => {
|
|
152
177
|
// github-copilot / codex shape: input is the max prompt, context the total
|
|
153
178
|
// window. Pressure math must use the input cap (OpenCode's own overflow.ts
|
|
@@ -211,10 +211,11 @@ function modelKeyLookupOrder(providerID: string, modelID: string): string[] {
|
|
|
211
211
|
return [...new Set(candidates)];
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
/** Resolve the user-tier output reservation for one runtime model. */
|
|
215
|
+
export function resolveOutputReserve(
|
|
216
216
|
providerID: string,
|
|
217
217
|
modelID: string,
|
|
218
|
+
config: OutputReserveConfig | undefined = outputReserveConfig,
|
|
218
219
|
): number | undefined {
|
|
219
220
|
if (typeof config === "number")
|
|
220
221
|
return Number.isFinite(config) && config >= 0 ? config : undefined;
|
|
@@ -259,7 +260,7 @@ export function resolveLimit(
|
|
|
259
260
|
if (input !== undefined && (context === undefined || input < context)) return input;
|
|
260
261
|
if (context === undefined) return undefined;
|
|
261
262
|
|
|
262
|
-
const configuredReserve =
|
|
263
|
+
const configuredReserve = resolveOutputReserve(providerID, modelID, reserveConfig);
|
|
263
264
|
let reserve: number;
|
|
264
265
|
if (configuredReserve !== undefined) {
|
|
265
266
|
reserve = configuredReserve;
|
|
@@ -523,7 +524,7 @@ export function getSdkWindowGeometry(
|
|
|
523
524
|
},
|
|
524
525
|
{
|
|
525
526
|
overlay: resolveWindowOverlayFacts(providerID, modelID, getWindowOverlay()),
|
|
526
|
-
|
|
527
|
+
outputReserveOverride: resolveOutputReserve(providerID, modelID),
|
|
527
528
|
harness: options?.harness ?? "opencode",
|
|
528
529
|
contextCap:
|
|
529
530
|
promptOnlyDetected === undefined && isFinitePositive(detectedContextLimit)
|
package/src/shared/rpc-server.ts
CHANGED
package/src/shared/rpc-types.ts
CHANGED
|
@@ -9,6 +9,20 @@ import type {
|
|
|
9
9
|
} from "../features/magic-context/dreamer/task-registry";
|
|
10
10
|
import type { LoggerDiagnostics } from "./logger";
|
|
11
11
|
|
|
12
|
+
export interface TailHygieneStatus {
|
|
13
|
+
/** Tokens in active, non-protected tail content that the agent can reclaim. */
|
|
14
|
+
u: number;
|
|
15
|
+
/** Tokens in rendered-tail content eligible for hygiene accounting in the same scan. */
|
|
16
|
+
t: number;
|
|
17
|
+
/** Reclaimable-to-eligible token ratio, clamped to 0–1 and shared by both nudge mechanisms. */
|
|
18
|
+
severity: number;
|
|
19
|
+
/** False until a fresh scan runs after existing tail content changes, preventing stale measurements. */
|
|
20
|
+
evaluable: boolean;
|
|
21
|
+
generationInvalidated: boolean;
|
|
22
|
+
baselineGeneration: number;
|
|
23
|
+
computedAt: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
export interface SidebarSnapshot {
|
|
13
27
|
sessionId: string;
|
|
14
28
|
usagePercentage: number;
|
|
@@ -78,6 +92,8 @@ export interface SidebarSnapshot {
|
|
|
78
92
|
* shows this as "Tool Definitions".
|
|
79
93
|
*/
|
|
80
94
|
toolDefinitionTokens: number;
|
|
95
|
+
/** Persisted reclaimable (U) and eligible (T) token counts used by both nudge mechanisms. */
|
|
96
|
+
tailHygiene?: TailHygieneStatus;
|
|
81
97
|
/**
|
|
82
98
|
* Effective execute-threshold percentage for this session's active model,
|
|
83
99
|
* after per-model resolution and the tokens→percentage conversion (when
|
|
@@ -6,6 +6,7 @@ import type { readFileSync } from "node:fs";
|
|
|
6
6
|
import {
|
|
7
7
|
__resetRpcIdentityTestHooks,
|
|
8
8
|
__setRpcIdentityTestHooks,
|
|
9
|
+
classifyProcessKind,
|
|
9
10
|
discoverLivePiProcessIds,
|
|
10
11
|
inspectLivePiProcesses,
|
|
11
12
|
isPidAlive,
|
|
@@ -54,6 +55,30 @@ afterEach(() => {
|
|
|
54
55
|
__resetRpcIdentityTestHooks();
|
|
55
56
|
});
|
|
56
57
|
|
|
58
|
+
describe("classifyProcessKind", () => {
|
|
59
|
+
test("classifies OpenCode server, OpenCode instance, Pi, and unknown commands", () => {
|
|
60
|
+
expect(classifyProcessKind("/usr/local/bin/opencode serve --hostname 127.0.0.1")).toBe(
|
|
61
|
+
"OpenCode server",
|
|
62
|
+
);
|
|
63
|
+
expect(classifyProcessKind("node /opt/opencode/bin/opencode --continue")).toBe(
|
|
64
|
+
"OpenCode instance (TUI/CLI)",
|
|
65
|
+
);
|
|
66
|
+
expect(
|
|
67
|
+
classifyProcessKind(
|
|
68
|
+
"bun /workspace/node_modules/@mariozechner/pi-coding-agent/dist/cli.js",
|
|
69
|
+
),
|
|
70
|
+
).toBe("Pi");
|
|
71
|
+
expect(classifyProcessKind("/usr/bin/other --flag")).toBe("process");
|
|
72
|
+
expect(classifyProcessKind(null)).toBe("process");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("recognizes serve flags and Windows-style executable paths", () => {
|
|
76
|
+
expect(classifyProcessKind("opencode --serve=true")).toBe("OpenCode server");
|
|
77
|
+
expect(classifyProcessKind("C:\\Tools\\opencode.exe")).toBe("OpenCode instance (TUI/CLI)");
|
|
78
|
+
expect(classifyProcessKind("pi.cmd --model test")).toBe("Pi");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
57
82
|
describe("discoverLivePiProcessIds", () => {
|
|
58
83
|
test("finds Pi-family harness commands while excluding the current process", () => {
|
|
59
84
|
__setRpcIdentityTestHooks({
|
package/src/shared/rpc-utils.ts
CHANGED
|
@@ -3,10 +3,16 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
+
export type ProcessKind = "OpenCode server" | "OpenCode instance (TUI/CLI)" | "Pi" | "process";
|
|
7
|
+
|
|
6
8
|
export interface RpcPortFileRecord {
|
|
7
9
|
port: number;
|
|
8
10
|
pid: number;
|
|
9
11
|
started_at: number;
|
|
12
|
+
/** Optional producer-provided kind; older records omit it. */
|
|
13
|
+
kind?: string;
|
|
14
|
+
/** Compatibility with discovery records that used the harness name. */
|
|
15
|
+
harness?: string;
|
|
10
16
|
/**
|
|
11
17
|
* Per-process bearer token. The server requires it on all non-health RPC
|
|
12
18
|
* calls so a random local process or browser-origin script that merely
|
|
@@ -224,6 +230,78 @@ function readPsProcessCommand(pid: number): string | null {
|
|
|
224
230
|
}
|
|
225
231
|
}
|
|
226
232
|
|
|
233
|
+
/** Reuse the platform-gated command probes used by PID identity checks. */
|
|
234
|
+
export function readProcessCommand(pid: number): string | null {
|
|
235
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
236
|
+
return rpcIdentityPlatform === "linux"
|
|
237
|
+
? readLinuxProcessCommand(pid)
|
|
238
|
+
: rpcIdentityPlatform === "win32"
|
|
239
|
+
? (readWindowsProcess(pid).command ?? null)
|
|
240
|
+
: readPsProcessCommand(pid);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function executableName(token: string | undefined): string {
|
|
244
|
+
return (
|
|
245
|
+
(token ?? "")
|
|
246
|
+
.replace(/^['"]|['"]$/g, "")
|
|
247
|
+
.split("/")
|
|
248
|
+
.at(-1) ?? ""
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function commandTokens(command: string): string[] {
|
|
253
|
+
return command
|
|
254
|
+
.toLowerCase()
|
|
255
|
+
.replaceAll("\\", "/")
|
|
256
|
+
.replaceAll("\u0000", " ")
|
|
257
|
+
.split(/\s+/)
|
|
258
|
+
.map((token) => token.replace(/^['"]|['"]$/g, ""))
|
|
259
|
+
.filter(Boolean);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function commandHasOpenCodeExecutable(tokens: readonly string[]): number {
|
|
263
|
+
return tokens.findIndex((token) => {
|
|
264
|
+
const executable = executableName(token).replace(/\.(?:exe|cmd)$/, "");
|
|
265
|
+
return executable === "opencode" || executable.endsWith("/opencode");
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function commandHasPiExecutable(tokens: readonly string[]): boolean {
|
|
270
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
271
|
+
const executable = executableName(tokens[index]).replace(/\.(?:exe|cmd)$/, "");
|
|
272
|
+
if (["pi", "omp", "oh-my-pi"].includes(executable)) return true;
|
|
273
|
+
if (["node", "bun", "deno"].includes(executable)) {
|
|
274
|
+
const script = executableName(tokens[index + 1]).replace(/\.(?:exe|cmd)$/, "");
|
|
275
|
+
if (
|
|
276
|
+
["pi", "pi.js", "pi.mjs", "pi.cjs"].includes(script) ||
|
|
277
|
+
tokens[index + 1]?.includes("pi-coding-agent")
|
|
278
|
+
) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Classify a process command without changing the liveness decision. */
|
|
287
|
+
export function classifyProcessKind(command: string | null | undefined): ProcessKind {
|
|
288
|
+
if (!command) return "process";
|
|
289
|
+
const tokens = commandTokens(command);
|
|
290
|
+
const openCodeIndex = commandHasOpenCodeExecutable(tokens);
|
|
291
|
+
if (openCodeIndex >= 0) {
|
|
292
|
+
const args = tokens.slice(openCodeIndex + 1);
|
|
293
|
+
if (
|
|
294
|
+
args.some(
|
|
295
|
+
(token) => token === "serve" || token === "--serve" || token.startsWith("--serve="),
|
|
296
|
+
)
|
|
297
|
+
) {
|
|
298
|
+
return "OpenCode server";
|
|
299
|
+
}
|
|
300
|
+
return "OpenCode instance (TUI/CLI)";
|
|
301
|
+
}
|
|
302
|
+
return commandHasPiExecutable(tokens) ? "Pi" : "process";
|
|
303
|
+
}
|
|
304
|
+
|
|
227
305
|
function commandLooksLikeOpenCode(command: string): boolean {
|
|
228
306
|
const normalized = command.toLowerCase();
|
|
229
307
|
return OPEN_CODE_COMMAND_MARKERS.some((marker) => normalized.includes(marker));
|
|
@@ -394,6 +472,8 @@ export function parseRpcPortFile(content: string, fallbackPid = 0): RpcPortFileR
|
|
|
394
472
|
port,
|
|
395
473
|
pid,
|
|
396
474
|
started_at: Number.isFinite(startedAt) ? startedAt : 0,
|
|
475
|
+
kind: typeof parsed.kind === "string" ? parsed.kind : undefined,
|
|
476
|
+
harness: typeof parsed.harness === "string" ? parsed.harness : undefined,
|
|
397
477
|
token: typeof parsed.token === "string" ? parsed.token : undefined,
|
|
398
478
|
instance_id:
|
|
399
479
|
typeof parsed.instance_id === "string" ? parsed.instance_id : undefined,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Channel1State } from "../hooks/magic-context/ctx-reduce-nudge";
|
|
2
|
+
import type { TailHygieneStatus } from "./rpc-types";
|
|
3
|
+
|
|
4
|
+
export interface WireTailHygieneBaseline {
|
|
5
|
+
u?: number;
|
|
6
|
+
t?: number;
|
|
7
|
+
severity?: number;
|
|
8
|
+
evaluable?: boolean;
|
|
9
|
+
generation_invalidated?: boolean;
|
|
10
|
+
baseline_generation?: number;
|
|
11
|
+
computed_at_ms?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function finiteNumber(value: unknown, fallback = 0): number {
|
|
15
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Normalize either renderer authority's persisted baseline without dropping valid zeros. */
|
|
19
|
+
export function resolveTailHygieneStatus(
|
|
20
|
+
tsBaseline: Channel1State | undefined,
|
|
21
|
+
rustBaseline?: WireTailHygieneBaseline | null,
|
|
22
|
+
): TailHygieneStatus | undefined {
|
|
23
|
+
if (rustBaseline !== undefined && rustBaseline !== null) {
|
|
24
|
+
const t = Math.max(0, finiteNumber(rustBaseline.t));
|
|
25
|
+
const u = Math.min(t, Math.max(0, finiteNumber(rustBaseline.u)));
|
|
26
|
+
return {
|
|
27
|
+
u,
|
|
28
|
+
t,
|
|
29
|
+
severity: Math.min(
|
|
30
|
+
1,
|
|
31
|
+
Math.max(0, finiteNumber(rustBaseline.severity, u / Math.max(t, 1))),
|
|
32
|
+
),
|
|
33
|
+
evaluable: rustBaseline.evaluable === true,
|
|
34
|
+
generationInvalidated: rustBaseline.generation_invalidated === true,
|
|
35
|
+
baselineGeneration: Math.max(0, finiteNumber(rustBaseline.baseline_generation)),
|
|
36
|
+
computedAt: Math.max(0, finiteNumber(rustBaseline.computed_at_ms)),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (tsBaseline === undefined) return undefined;
|
|
40
|
+
const t = Math.max(0, tsBaseline.baselineT + tsBaseline.turnDeltaT);
|
|
41
|
+
const u = Math.min(t, Math.max(0, tsBaseline.baselineU + tsBaseline.turnDeltaU));
|
|
42
|
+
return {
|
|
43
|
+
u,
|
|
44
|
+
t,
|
|
45
|
+
severity: Math.min(1, Math.max(0, u / Math.max(t, 1))),
|
|
46
|
+
evaluable: tsBaseline.evaluable && !tsBaseline.generationInvalidated,
|
|
47
|
+
generationInvalidated: tsBaseline.generationInvalidated,
|
|
48
|
+
baselineGeneration: Math.max(0, tsBaseline.baselineGeneration),
|
|
49
|
+
computedAt: Math.max(0, tsBaseline.computedAt),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatTailHygiene(status: TailHygieneStatus): string {
|
|
54
|
+
const percentage = (status.severity * 100).toFixed(1);
|
|
55
|
+
const state = status.evaluable ? "" : " · held until baseline refresh";
|
|
56
|
+
return `${percentage}% · ${status.u.toLocaleString()} / ${status.t.toLocaleString()} tok${state}`;
|
|
57
|
+
}
|
|
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { resolveLimit } from "./models-dev-cache";
|
|
5
|
+
import { resolveLimit, resolveOutputReserve } from "./models-dev-cache";
|
|
6
6
|
import {
|
|
7
7
|
deriveWindowGeometry,
|
|
8
8
|
formatWindowDerivationLine,
|
|
@@ -240,6 +240,39 @@ describe("window geometry", () => {
|
|
|
240
240
|
expect(result?.usableSoft).toBe(1_016_576);
|
|
241
241
|
});
|
|
242
242
|
|
|
243
|
+
test("output_reserve forms override a pre-carved input and overlay output facts", () => {
|
|
244
|
+
const data = overlay("openai-codex", "gpt-5.6-sol", {
|
|
245
|
+
"output.enforced": fact({ kind: "stated", value: 68_000 }),
|
|
246
|
+
});
|
|
247
|
+
const cases = [
|
|
248
|
+
16_384,
|
|
249
|
+
{ default: 16_384 },
|
|
250
|
+
{ default: 32_000, "openai-codex/gpt-5.6-sol": 16_384 },
|
|
251
|
+
] as const;
|
|
252
|
+
|
|
253
|
+
for (const reserveConfig of cases) {
|
|
254
|
+
const result = deriveWindowGeometry(
|
|
255
|
+
"openai-codex",
|
|
256
|
+
"gpt-5.6-sol",
|
|
257
|
+
{ context: 400_000, input: 272_000, output: 128_000 },
|
|
258
|
+
{
|
|
259
|
+
overlay: resolveWindowOverlayFacts("openai-codex", "gpt-5.6-sol", data),
|
|
260
|
+
outputReserveOverride: resolveOutputReserve(
|
|
261
|
+
"openai-codex",
|
|
262
|
+
"gpt-5.6-sol",
|
|
263
|
+
reserveConfig,
|
|
264
|
+
),
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
expect(result?.usableSoft).toBe(272_000 - 16_384);
|
|
268
|
+
expect(result?.derivation).toMatchObject({
|
|
269
|
+
window: 272_000,
|
|
270
|
+
reserve: 16_384,
|
|
271
|
+
reserveSource: "output_config",
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
243
276
|
test("provider hook wins over overlay, while overlay beats catalog", () => {
|
|
244
277
|
const data = overlay("provider", "model", {
|
|
245
278
|
"window.enforced": fact({ kind: "stated", value: 300_000 }),
|
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { getDataDir } from "./data-path";
|
|
4
4
|
import { modelRefLookupOrder } from "./harness-provider-map";
|
|
5
5
|
import { sessionLog } from "./logger";
|
|
6
|
-
import type { ModelLimit
|
|
6
|
+
import type { ModelLimit } from "./models-dev-cache";
|
|
7
7
|
|
|
8
8
|
export const WINDOW_OVERLAY_SCHEMA = "fusiform-window-overlay/v1";
|
|
9
9
|
export const PROMPT_WALL_MARGIN = 4_096;
|
|
@@ -88,7 +88,8 @@ export interface DeriveWindowGeometryOptions {
|
|
|
88
88
|
overlay?: ResolvedWindowOverlayFacts;
|
|
89
89
|
/** A provider/auth hook has higher precedence than the overlay, field by field. */
|
|
90
90
|
providerLimit?: ModelLimit;
|
|
91
|
-
|
|
91
|
+
/** Resolved user override; higher precedence than catalog, overlay, and geometry defaults. */
|
|
92
|
+
outputReserveOverride?: number;
|
|
92
93
|
harness?: "opencode" | "pi";
|
|
93
94
|
/** Detected overflow remains the final downward cap regardless of other sources. */
|
|
94
95
|
contextCap?: number;
|
|
@@ -139,32 +140,6 @@ function isFinitePositive(value: unknown): value is number {
|
|
|
139
140
|
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
function modelKeyLookupOrder(providerID: string, modelID: string): string[] {
|
|
143
|
-
const candidates = [...modelRefLookupOrder(`${providerID}/${modelID}`), modelID];
|
|
144
|
-
const colon = modelID.lastIndexOf(":");
|
|
145
|
-
if (colon > 0) {
|
|
146
|
-
const bareModel = modelID.slice(0, colon);
|
|
147
|
-
candidates.push(...modelRefLookupOrder(`${providerID}/${bareModel}`), bareModel);
|
|
148
|
-
}
|
|
149
|
-
return [...new Set(candidates)];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function configuredOutputReserve(
|
|
153
|
-
config: OutputReserveConfig | undefined,
|
|
154
|
-
providerID: string,
|
|
155
|
-
modelID: string,
|
|
156
|
-
): number | undefined {
|
|
157
|
-
if (typeof config === "number") {
|
|
158
|
-
return Number.isFinite(config) && config >= 0 ? config : undefined;
|
|
159
|
-
}
|
|
160
|
-
if (!config) return undefined;
|
|
161
|
-
for (const candidate of modelKeyLookupOrder(providerID, modelID)) {
|
|
162
|
-
const value = config[candidate];
|
|
163
|
-
if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
|
|
164
|
-
}
|
|
165
|
-
return Number.isFinite(config.default) && config.default >= 0 ? config.default : undefined;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
143
|
/** Convert the ratified tagged numeric union into the conservative scalar used by derivation. */
|
|
169
144
|
export function scalarizeFact(value: WindowOverlayFactValue): number | undefined {
|
|
170
145
|
if (value.kind === "stated") return isFinitePositive(value.value) ? value.value : undefined;
|
|
@@ -509,25 +484,46 @@ export function deriveWindowGeometry(
|
|
|
509
484
|
: (PROVIDER_GEOMETRY[providerID] ?? "shared_upfront");
|
|
510
485
|
const geometryOverride = geometryFact?.kind === "stated" ? geometryFact.value : undefined;
|
|
511
486
|
|
|
487
|
+
const preCarvedInput =
|
|
488
|
+
isFinitePositive(input) && (!isFinitePositive(softContext) || input < softContext);
|
|
489
|
+
const outputReserveOverride =
|
|
490
|
+
typeof options.outputReserveOverride === "number" &&
|
|
491
|
+
Number.isFinite(options.outputReserveOverride) &&
|
|
492
|
+
options.outputReserveOverride >= 0
|
|
493
|
+
? options.outputReserveOverride
|
|
494
|
+
: undefined;
|
|
495
|
+
let derivationWindow = softContext ?? input;
|
|
512
496
|
let usableSoft: number;
|
|
513
497
|
let softReserve = 0;
|
|
514
498
|
let reserveSource: WindowReserveSource = "none";
|
|
515
|
-
if (
|
|
499
|
+
if (outputReserveOverride !== undefined) {
|
|
500
|
+
// An explicit user reserve owns the usable-window carve. When the
|
|
501
|
+
// provider also publishes a smaller input cap, use that cap as the
|
|
502
|
+
// window; treating the cap as final would silently ignore the config.
|
|
503
|
+
const reserveWindow = preCarvedInput ? input : (softContext ?? input);
|
|
504
|
+
if (!isFinitePositive(reserveWindow)) return undefined;
|
|
505
|
+
derivationWindow = reserveWindow;
|
|
506
|
+
softReserve = outputReserveOverride;
|
|
507
|
+
reserveSource = "output_config";
|
|
508
|
+
const floor = Math.max(MIN_PLAUSIBLE_CONTEXT_LIMIT, reserveWindow * 0.5);
|
|
509
|
+
const flooredReserve = Math.min(softReserve, Math.max(0, reserveWindow - floor));
|
|
510
|
+
if (flooredReserve < softReserve) {
|
|
511
|
+
logGeometryClampOnce(
|
|
512
|
+
`soft-floor|${providerID}/${modelID}|${softReserve}|${flooredReserve}`,
|
|
513
|
+
`output reserve clamped by the half-window floor for ${providerID}/${modelID}: reserve ${softReserve} → ${flooredReserve}`,
|
|
514
|
+
options.log,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
softReserve = flooredReserve;
|
|
518
|
+
usableSoft = Math.floor(reserveWindow - softReserve);
|
|
519
|
+
} else if (preCarvedInput) {
|
|
516
520
|
usableSoft = input;
|
|
517
521
|
if (isFinitePositive(softContext)) {
|
|
518
522
|
softReserve = Math.max(0, softContext - input);
|
|
519
523
|
reserveSource = "output_catalog";
|
|
520
524
|
}
|
|
521
525
|
} else if (isFinitePositive(softContext)) {
|
|
522
|
-
|
|
523
|
-
options.reserveConfig,
|
|
524
|
-
providerID,
|
|
525
|
-
modelID,
|
|
526
|
-
);
|
|
527
|
-
if (configuredReserve !== undefined) {
|
|
528
|
-
softReserve = configuredReserve;
|
|
529
|
-
reserveSource = "output_config";
|
|
530
|
-
} else if (
|
|
526
|
+
if (
|
|
531
527
|
geometry === "separate" &&
|
|
532
528
|
(options.overlay === undefined ||
|
|
533
529
|
options.harness === "pi" ||
|
|
@@ -591,13 +587,14 @@ export function deriveWindowGeometry(
|
|
|
591
587
|
usableHard = usableSoft;
|
|
592
588
|
}
|
|
593
589
|
|
|
590
|
+
const resolvedWindow = derivationWindow ?? usableSoft;
|
|
594
591
|
return {
|
|
595
592
|
usableSoft,
|
|
596
593
|
usableHard,
|
|
597
594
|
geometry,
|
|
598
595
|
derivation: {
|
|
599
|
-
window: Math.floor(
|
|
600
|
-
reserve: Math.floor(Math.max(0,
|
|
596
|
+
window: Math.floor(resolvedWindow),
|
|
597
|
+
reserve: Math.floor(Math.max(0, resolvedWindow - usableSoft)),
|
|
601
598
|
reserveSource,
|
|
602
599
|
geometry,
|
|
603
600
|
},
|
package/src/tui/index.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import packageJson from "../../package.json"
|
|
|
11
11
|
import { closeRpc, dismissUpgradeReminder, getAnnouncement, getCompartmentCount, getRpcGeneration, initRpcClient, loadEmbedDetail, loadStatusDetail, loadToastDurationMs, markAnnounced, requestRecomp, requestUpgrade, type EmbedDetail, type StatusDetail } from "./data/context-db"
|
|
12
12
|
import { startNotificationSocket, stopNotificationSocket, type SocketNotification } from "./data/notification-socket"
|
|
13
13
|
import { formatThresholdPercent } from "../shared/format-threshold"
|
|
14
|
+
import { formatTailHygiene } from "../shared/tail-hygiene-status"
|
|
14
15
|
import { formatWindowDerivationLine } from "../shared/window-geometry"
|
|
15
16
|
import { compactionOffSidebarRows, nativeCompactionContextLabel } from "./compaction-off"
|
|
16
17
|
import { isCompactionEnabled } from "../config/agent-disable"
|
|
@@ -202,7 +203,7 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
|
|
|
202
203
|
segs.push({ label: "User Profile", tokens: d.profileTokens, color: COLORS.profile })
|
|
203
204
|
|
|
204
205
|
if (d.conversationTokens > 0)
|
|
205
|
-
segs.push({ label: "Conversation", tokens: d.conversationTokens, color: COLORS.conversation })
|
|
206
|
+
segs.push({ label: "Conversation*", tokens: d.conversationTokens, color: COLORS.conversation })
|
|
206
207
|
if (d.toolCallTokens > 0)
|
|
207
208
|
segs.push({ label: "Tool Calls", tokens: d.toolCallTokens, color: COLORS.toolCalls })
|
|
208
209
|
if (d.toolDefinitionTokens > 0)
|
|
@@ -271,6 +272,15 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
|
|
|
271
272
|
</box>
|
|
272
273
|
)
|
|
273
274
|
})}
|
|
275
|
+
<text fg={t().textMuted}>* Conversation includes Reasoning; hygiene excludes it</text>
|
|
276
|
+
{s().tailHygiene !== undefined && (
|
|
277
|
+
<R
|
|
278
|
+
t={t()}
|
|
279
|
+
l="Hygiene"
|
|
280
|
+
v={formatTailHygiene(s().tailHygiene!)}
|
|
281
|
+
fg={s().tailHygiene!.evaluable ? t().accent : t().warning}
|
|
282
|
+
/>
|
|
283
|
+
)}
|
|
274
284
|
</box>
|
|
275
285
|
|
|
276
286
|
{/* Recomp / session-upgrade live progress (full width, only while
|
|
@@ -346,7 +356,7 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
|
|
|
346
356
|
</box>
|
|
347
357
|
<R t={t()} l="Configured" v={s().cacheTtl} />
|
|
348
358
|
<R t={t()} l="Last response" v={s().lastResponseTime > 0 ? `${Math.round(elapsed() / 1000)}s ago` : "never"} />
|
|
349
|
-
<R t={t()} l="Remaining" v={s().cacheExpired ? "expired" : s().cacheNeverExpires ? "never
|
|
359
|
+
<R t={t()} l="Remaining" v={s().cacheExpired ? "expired" : s().cacheNeverExpires ? "never (MC never assumes expiry — external cache-keep)" : `${Math.round(s().cacheRemainingMs / 1000)}s`} fg={s().cacheExpired ? t().warning : t().textMuted} />
|
|
350
360
|
<R t={t()} l="Auto-execute" v={s().cacheExpired ? "yes (expired)" : s().cacheNeverExpires ? `at ≥${formatThresholdPercent(s().executeThreshold)}%` : `at TTL or ≥${formatThresholdPercent(s().executeThreshold)}%`} fg={t().textMuted} />
|
|
351
361
|
<box marginTop={1}>
|
|
352
362
|
<text fg={t().text}><b>Memory</b></text>
|
|
@@ -5,6 +5,7 @@ import packageJson from "../../../package.json"
|
|
|
5
5
|
import { badgeTextColor } from '../badge-contrast';
|
|
6
6
|
import { loadSidebarSnapshot, type SidebarSnapshot } from "../data/context-db"
|
|
7
7
|
import { formatThresholdPercent } from "../../shared/format-threshold"
|
|
8
|
+
import { formatTailHygiene } from "../../shared/tail-hygiene-status"
|
|
8
9
|
import { compactionOffSidebarRows, nativeCompactionContextLabel } from "../compaction-off"
|
|
9
10
|
import {
|
|
10
11
|
computeEffectiveOrder,
|
|
@@ -239,7 +240,7 @@ const TokenBreakdown = (props: {
|
|
|
239
240
|
key: "conv",
|
|
240
241
|
tokens: s.conversationTokens,
|
|
241
242
|
color: COLORS.conversation,
|
|
242
|
-
label: "Conversation",
|
|
243
|
+
label: "Conversation*",
|
|
243
244
|
})
|
|
244
245
|
|
|
245
246
|
// Tool Calls = tool_use/tool_result/tool/tool-invocation parts in messages
|
|
@@ -320,6 +321,7 @@ const TokenBreakdown = (props: {
|
|
|
320
321
|
</box>
|
|
321
322
|
)
|
|
322
323
|
})}
|
|
324
|
+
<text fg={props.theme.textMuted}>* includes Reasoning; hygiene excludes it</text>
|
|
323
325
|
</box>
|
|
324
326
|
)}
|
|
325
327
|
</box>
|
|
@@ -787,16 +789,26 @@ const SidebarContent = (props: {
|
|
|
787
789
|
<b>{s()!.usagePercentage.toFixed(1)}%</b> / {formatThresholdPercent(s()!.executeThreshold)}%{s()!.executeThresholdClamped ? "*" : ""}
|
|
788
790
|
</text>
|
|
789
791
|
)}
|
|
790
|
-
{/* Right: absolute token usage
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
much headroom remains beyond compaction). */}
|
|
792
|
+
{/* Right: absolute token usage against the usable
|
|
793
|
+
scheduler window — the same denominator as the
|
|
794
|
+
percentage and nudge/trigger scheduling. */}
|
|
794
795
|
<text fg={contextSummaryColor()}>
|
|
795
796
|
{compactTokens(s()!.inputTokens)} / {compactTokens(s()!.contextLimit)}
|
|
796
797
|
</text>
|
|
797
798
|
</box>
|
|
798
799
|
)}
|
|
799
800
|
<TokenBreakdown theme={props.theme} snapshot={s()!} collapsed={collapsed()} />
|
|
801
|
+
{!collapsed() && (
|
|
802
|
+
<text fg={props.theme.textMuted}>Conversation includes reasoning estimates; hygiene excludes reasoning.</text>
|
|
803
|
+
)}
|
|
804
|
+
{s()!.tailHygiene !== undefined && (
|
|
805
|
+
<StatRow
|
|
806
|
+
theme={props.theme}
|
|
807
|
+
label="Hygiene"
|
|
808
|
+
value={formatTailHygiene(s()!.tailHygiene!)}
|
|
809
|
+
warning={!s()!.tailHygiene!.evaluable}
|
|
810
|
+
/>
|
|
811
|
+
)}
|
|
800
812
|
</box>
|
|
801
813
|
)}
|
|
802
814
|
|
|
@@ -49,6 +49,9 @@ describe("compiled TUI runtime imports", () => {
|
|
|
49
49
|
* Built from the shared list so the test cannot cover fewer modules than the
|
|
50
50
|
* build rewrites. */
|
|
51
51
|
async function loadExportSets(): Promise<Record<string, Set<string>>> {
|
|
52
|
+
// @opentui/core/testing subclasses a core export during module initialization;
|
|
53
|
+
// warm core before the parallel imports so Bun cannot expose its TDZ.
|
|
54
|
+
await import("@opentui/core");
|
|
52
55
|
const entries = await Promise.all(
|
|
53
56
|
TUI_RUNTIME_SPECIFIERS.map(
|
|
54
57
|
async (specifier) =>
|