@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.
Files changed (150) hide show
  1. package/README.md +2 -0
  2. package/dist/{announcement-6c1g4es4.js → announcement-5eg4t64s.js} +6 -6
  3. package/dist/config/schema/magic-context.d.ts +1 -1
  4. package/dist/{conflict-warning-hook-an4fxhwf.js → conflict-warning-hook-f406ggbw.js} +5 -5
  5. package/dist/{derive-budgets-0f8xpyw2.js → derive-budgets-xk8cy5e6.js} +5 -5
  6. package/dist/{emscripten-module-ap8t3st0.js → emscripten-module-49k43zfy.js} +1 -1
  7. package/dist/{emscripten-module-682pfzaa.js → emscripten-module-ex4v0x4z.js} +1 -1
  8. package/dist/{emscripten-module-r5h7aghp.js → emscripten-module-jfs3d1dq.js} +1 -1
  9. package/dist/{emscripten-module-bkktz927.js → emscripten-module-ss4g4spn.js} +1 -1
  10. package/dist/features/magic-context/dreamer/classify-prompt.d.ts +5 -1
  11. package/dist/features/magic-context/dreamer/classify-prompt.d.ts.map +1 -1
  12. package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
  13. package/dist/features/magic-context/dreamer/manifest-parser.d.ts +8 -0
  14. package/dist/features/magic-context/dreamer/manifest-parser.d.ts.map +1 -1
  15. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +7 -1
  16. package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
  17. package/dist/features/magic-context/dreamer/map-memories.d.ts +18 -0
  18. package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
  19. package/dist/features/magic-context/dreamer/retrospective-orphan-sweep.d.ts +17 -6
  20. package/dist/features/magic-context/dreamer/retrospective-orphan-sweep.d.ts.map +1 -1
  21. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +5 -0
  22. package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
  23. package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
  24. package/dist/features/magic-context/dreamer/verify-prompt.d.ts +5 -1
  25. package/dist/features/magic-context/dreamer/verify-prompt.d.ts.map +1 -1
  26. package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
  27. package/dist/features/magic-context/fail-closed-block.d.ts +6 -1
  28. package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
  29. package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
  30. package/dist/features/magic-context/memory/types.d.ts +16 -0
  31. package/dist/features/magic-context/memory/types.d.ts.map +1 -1
  32. package/dist/features/magic-context/message-index.d.ts +3 -2
  33. package/dist/features/magic-context/message-index.d.ts.map +1 -1
  34. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  35. package/dist/features/magic-context/reclaim-protection.d.ts +13 -0
  36. package/dist/features/magic-context/reclaim-protection.d.ts.map +1 -0
  37. package/dist/features/magic-context/storage-db.d.ts +7 -2
  38. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  39. package/dist/features/magic-context/storage-embedding-measurements.d.ts +1 -1
  40. package/dist/features/magic-context/storage-embedding-measurements.d.ts.map +1 -1
  41. package/dist/features/magic-context/storage-meta-persisted.d.ts +7 -6
  42. package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
  43. package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
  44. package/dist/features/magic-context/storage-session-tables.d.ts +18 -0
  45. package/dist/features/magic-context/storage-session-tables.d.ts.map +1 -0
  46. package/dist/features/magic-context/storage-tags.d.ts +13 -5
  47. package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
  48. package/dist/features/magic-context/transform-decision-log.d.ts +22 -0
  49. package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
  50. package/dist/{ffi-d8pnn3mn.js → ffi-bbm9vkfa.js} +1 -1
  51. package/dist/{ffi-04jzypzk.js → ffi-kb5r0x1j.js} +1 -1
  52. package/dist/{ffi-t8b862vx.js → ffi-qnrt0y78.js} +1 -1
  53. package/dist/{ffi-9wvt2wwz.js → ffi-yx2hpf4p.js} +1 -1
  54. package/dist/hooks/auto-update-checker/checker.d.ts +8 -1
  55. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  56. package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
  57. package/dist/hooks/magic-context/channel2-cycle.d.ts +16 -0
  58. package/dist/hooks/magic-context/channel2-cycle.d.ts.map +1 -0
  59. package/dist/hooks/magic-context/channel2-delivery.d.ts +3 -9
  60. package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
  61. package/dist/hooks/magic-context/command-handler.d.ts +2 -0
  62. package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
  63. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +33 -103
  64. package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
  65. package/dist/hooks/magic-context/emergency-drop.d.ts.map +1 -1
  66. package/dist/hooks/magic-context/empty-task-output.d.ts +16 -0
  67. package/dist/hooks/magic-context/empty-task-output.d.ts.map +1 -0
  68. package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
  69. package/dist/hooks/magic-context/execute-status.d.ts +2 -1
  70. package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
  71. package/dist/hooks/magic-context/fold-execution-gate.d.ts +2 -0
  72. package/dist/hooks/magic-context/fold-execution-gate.d.ts.map +1 -0
  73. package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
  74. package/dist/hooks/magic-context/hook.d.ts.map +1 -1
  75. package/dist/hooks/magic-context/inject-compartments.d.ts +6 -0
  76. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  77. package/dist/hooks/magic-context/live-session-state.d.ts +3 -0
  78. package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
  79. package/dist/hooks/magic-context/rust-mode-transform.d.ts +4 -2
  80. package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
  81. package/dist/hooks/magic-context/strip-content.d.ts +10 -8
  82. package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
  83. package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -1
  84. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  85. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts +78 -0
  86. package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -0
  87. package/dist/hooks/magic-context/tool-reclaim.d.ts.map +1 -1
  88. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +7 -0
  89. package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
  90. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  91. package/dist/{index-cae89eas.js → index-1h9t9xb5.js} +2 -2
  92. package/dist/{index-wckvcay0.js → index-1yh8g550.js} +8 -6
  93. package/dist/index-22mw8km4.js +558 -0
  94. package/dist/{index-cgyfn1s2.js → index-2wchnyk6.js} +4 -2
  95. package/dist/{index-0ze9p983.js → index-5znjkjws.js} +9996 -7043
  96. package/dist/{index-4cz5nfxf.js → index-btj2qhmc.js} +5 -5
  97. package/dist/{index-q0t189ay.js → index-mg1yx1ph.js} +28 -36
  98. package/dist/{index-ayd60xnw.js → index-rgdqj8nj.js} +3 -3
  99. package/dist/{index-ygcng9t8.js → index-rpgg35ms.js} +50 -50
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +776 -846
  102. package/dist/{module-ES6BEMUI-atfbtnbp.js → module-ES6BEMUI-1yjdz59c.js} +4 -4
  103. package/dist/{module-asyncify-2EFITU5U-7xckhw8w.js → module-asyncify-2EFITU5U-fdkx4fhy.js} +1 -1
  104. package/dist/plugin/dream-timer.d.ts +5 -0
  105. package/dist/plugin/dream-timer.d.ts.map +1 -1
  106. package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
  107. package/dist/plugin/messages-transform.d.ts.map +1 -1
  108. package/dist/plugin/rpc-handlers.d.ts +2 -0
  109. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  110. package/dist/{prompt-context-jpe2dstn.js → prompt-context-29qkb28f.js} +1 -1
  111. package/dist/{recomp-orchestrator-c3ff16k7.js → recomp-orchestrator-p030kxtc.js} +15 -15
  112. package/dist/{resolve-fallbacks-5n5tvge1.js → resolve-fallbacks-qf0ekmz9.js} +3 -3
  113. package/dist/{rpc-notifications-4d22ybn5.js → rpc-notifications-1ffm755w.js} +6 -6
  114. package/dist/{safe-notification-target-b693f9x3.js → safe-notification-target-tepzc2xa.js} +3 -3
  115. package/dist/{send-session-notification-dxske5zm.js → send-session-notification-6v5b72b4.js} +7 -7
  116. package/dist/shared/harness-provider-map.d.ts +6 -0
  117. package/dist/shared/harness-provider-map.d.ts.map +1 -1
  118. package/dist/shared/models-dev-cache.d.ts +2 -0
  119. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  120. package/dist/shared/rpc-server.d.ts.map +1 -1
  121. package/dist/shared/rpc-types.d.ts +15 -0
  122. package/dist/shared/rpc-types.d.ts.map +1 -1
  123. package/dist/shared/rpc-utils.d.ts +9 -0
  124. package/dist/shared/rpc-utils.d.ts.map +1 -1
  125. package/dist/shared/tail-hygiene-status.d.ts +15 -0
  126. package/dist/shared/tail-hygiene-status.d.ts.map +1 -0
  127. package/dist/shared/window-geometry.d.ts +3 -2
  128. package/dist/shared/window-geometry.d.ts.map +1 -1
  129. package/dist/{storage-meta-session-mk4gsnke.js → storage-meta-session-2306t3j7.js} +8 -8
  130. package/dist/{task-config-gbp7r23g.js → task-config-at3adc59.js} +6 -6
  131. package/package.json +4 -2
  132. package/src/config/schema/magic-context.test.ts +1 -0
  133. package/src/config/schema/magic-context.ts +5 -5
  134. package/src/shared/harness-provider-map.test.ts +36 -0
  135. package/src/shared/harness-provider-map.ts +8 -0
  136. package/src/shared/models-dev-cache.test.ts +25 -0
  137. package/src/shared/models-dev-cache.ts +5 -4
  138. package/src/shared/rpc-server.ts +1 -0
  139. package/src/shared/rpc-types.ts +16 -0
  140. package/src/shared/rpc-utils.test.ts +25 -0
  141. package/src/shared/rpc-utils.ts +80 -0
  142. package/src/shared/tail-hygiene-status.ts +57 -0
  143. package/src/shared/window-geometry.test.ts +34 -1
  144. package/src/shared/window-geometry.ts +37 -40
  145. package/src/tui/index.tsx +12 -2
  146. package/src/tui/slots/sidebar-content.tsx +17 -5
  147. package/src/tui/tui-compiled-runtime-imports.test.ts +3 -0
  148. package/src/tui-compiled/index.tsx +312 -290
  149. package/src/tui-compiled/slots/sidebar-content.tsx +316 -287
  150. 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
- function configuredOutputReserve(
215
- config: OutputReserveConfig | undefined,
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 = configuredOutputReserve(reserveConfig, providerID, modelID);
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
- reserveConfig: outputReserveConfig,
527
+ outputReserveOverride: resolveOutputReserve(providerID, modelID),
527
528
  harness: options?.harness ?? "opencode",
528
529
  contextCap:
529
530
  promptOnlyDetected === undefined && isFinitePositive(detectedContextLimit)
@@ -185,6 +185,7 @@ export class MagicContextRpcServer {
185
185
  port: this.port,
186
186
  pid: process.pid,
187
187
  started_at: this.startedAt,
188
+ kind: "OpenCode server",
188
189
  token: this.token,
189
190
  instance_id: this.instanceId,
190
191
  }),
@@ -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({
@@ -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, OutputReserveConfig } from "./models-dev-cache";
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
- reserveConfig?: OutputReserveConfig;
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 (isFinitePositive(input) && (!isFinitePositive(softContext) || input < softContext)) {
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
- const configuredReserve = configuredOutputReserve(
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(softContext ?? usableSoft),
600
- reserve: Math.floor(Math.max(0, (softContext ?? usableSoft) - usableSoft)),
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 expires (always-warm lane)" : `${Math.round(s().cacheRemainingMs / 1000)}s`} fg={s().cacheExpired ? t().warning : t().textMuted} />
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 vs the model's
791
- full context window (separate from the
792
- execute threshold so users still know how
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) =>