Skip to content

Commit c1f3705

Browse files
committed
feat(config): execute_threshold_tokens for absolute-token execute thresholds
Adds execute_threshold_tokens as an alternative to execute_threshold_percentage. Per-model map (with optional 'default' key) of absolute token counts; when a model matches, tokens config overrides percentage and is clamped to 80% of the resolved context limit. Requires a known context_limit to convert to percentage. Core: - resolveExecuteThresholdDetail() is now the single source of truth returning { percentage, mode, absoluteTokens?, matchedKey? }. resolveExecuteThreshold() is a thin back-compat wrapper. - Clamp warnings dedupe by (session, model, tokenVal, cap) so over-cap configs don't spam the log on every transform pass. - Runtime guards (isFinitePositive) block NaN/negative/zero contextLimit and token values from poisoning the resolver; all bad inputs fall through to percentage config safely. Call-site integration: - scheduler.ts/transform.ts/event-handler.ts/hook.ts/create-session-hooks.ts: thread tokensConfig + contextLimit through the execute path. - execute-status.ts: /ctx-status now uses the detail resolver for mode display (fixes progressive base-model lookup drift where e.g. gpt-5.4-fast matching gpt-5.4 was mislabeled as percentage mode). - rpc-handlers.ts + rpc-types.ts: StatusDetail exposes executeThresholdMode and executeThresholdTokens so TUI can verify token mode and show the absolute trigger. - nudger.ts: intentionally stays percentage-only (advisory only; scheduler is authoritative). Documented inline. Config + docs: - schema: execute_threshold_tokens per-model map (min 5K, max 2M). - assets/magic-context.schema.json regenerated with drop_tool_structure, compaction_markers, and experimental fields that were previously missing. - CONFIGURATION.md: documents both percentage and token modes and their interaction. - dashboard ConfigEditor: new per-model text field for execute_threshold_tokens (alwaysObject + numericText so saved shape stays schema-valid); fixed drop_tool_structure default display, stale execute_threshold_percentage slider min (35→20), and added missing memory.retrieval_count_promotion_threshold. - PerModelField: adds alwaysObject and numericText props for fields whose schema forbids bare scalars or requires numeric values. Tests: +10 targeted cases for token-mode detail resolution, progressive base-model matching, clamp behavior, NaN/zero/negative guards, and dedupe stability. Suite is 571 pass (was 561).
1 parent b14f4dc commit c1f3705

19 files changed

Lines changed: 872 additions & 53 deletions

File tree

CONFIGURATION.md

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,13 @@ Higher-tier models with longer cache windows benefit from a longer TTL. Setting
6565
| `protected_tags` | `number` (1–100) | `20` | Last N active tags immune from immediate dropping. |
6666
| `nudge_interval_tokens` | `number` | `10000` | Minimum token growth between rolling nudges. |
6767
| `execute_threshold_percentage` | `number` (20–80) or `object` | `65` | Context usage that forces queued ops to execute. Capped at 80% max for cache safety. Supports per-model map. |
68+
| `execute_threshold_tokens` | `object` (per-model map) || **Optional absolute-tokens variant of `execute_threshold_percentage`.** Per-model map (e.g. `{ "default": 150000, "github-copilot/gpt-5.2-codex": 40000 }`). When set for a model, overrides the percentage-based threshold for that model. Clamped to `80% × context_limit` with a warn log. Requires a resolvable context limit — falls through to percentage if unavailable. See below. |
6869
| `auto_drop_tool_age` | `number` | `100` | Auto-drop tool outputs older than N tags during execution. |
70+
| `drop_tool_structure` | `boolean` | `true` | When `true`, dropped tool parts are fully removed from the transformed prompt. When `false`, tool call structure is preserved: tool name kept, tool inputs truncated to 5 chars + `...[truncated]`, tool output replaced with `[truncated]`. Preserving structure keeps the agent aware that prior tools ran (preventing hallucinated re-calls) at the cost of ~4K additional tokens per ~60 dropped tools. |
6971
| `clear_reasoning_age` | `number` | `50` | Clear thinking/reasoning blocks older than N tags. |
7072
| `iteration_nudge_threshold` | `number` | `15` | Consecutive assistant turns without user input before an iteration nudge. |
7173
| `historian_timeout_ms` | `number` | `300000` | Timeout per historian call (ms). |
72-
| `history_budget_percentage` | `number` (0–1) | `0.15` | Fraction of usable context reserved for the history block. Triggers compression when exceeded. |
74+
| `history_budget_percentage` | `number` (0.05–0.5) | `0.15` | Fraction of usable context (`context_limit × execute_threshold`) reserved for the history block. Triggers compression when exceeded. |
7375
| `compaction_markers` | `boolean` | `true` | Inject compaction boundaries into OpenCode's DB after historian publishes. Reduces transform input size for long sessions. |
7476
| `commit_cluster_trigger` | `object` | See below | Controls the commit-cluster historian trigger. |
7577

@@ -93,6 +95,37 @@ A **commit cluster** is a distinct work phase where the agent made one or more g
9395

9496
Set `enabled: false` to disable this trigger entirely and rely only on pressure-based and tail-size triggers for historian.
9597

98+
### `execute_threshold_tokens`
99+
100+
An absolute-tokens alternative to `execute_threshold_percentage`. Useful when you want a hard cap expressed in tokens rather than a percentage — for example, when a provider limits effective prompt size below its advertised context window.
101+
102+
```jsonc
103+
{
104+
"execute_threshold_tokens": {
105+
"default": 150000, // fires at 150K for any model without an explicit entry
106+
"github-copilot/gpt-5.2-codex": 40000 // fires at 40K specifically for gpt-5.2-codex
107+
}
108+
}
109+
```
110+
111+
**Behavior:**
112+
113+
- Per-model map only — no bare-number form. All sessions are assumed to have different context limits, so the `default` key acts as a fallback inside the map.
114+
- **Tokens wins:** when a matching entry exists for the current model, it overrides the percentage-based threshold for that model. Other models continue to use `execute_threshold_percentage`.
115+
- **Progressive key lookup** just like percentage config — `openai/gpt-5.4-fast` matches `openai/gpt-5.4` if the derived key is absent.
116+
- **Clamped at 80% × context_limit** for the same cache-safety reason as percentage. If the clamp fires, a `log.warn` records the original and capped value.
117+
- Requires a **resolvable context limit** at runtime. On brand-new sessions before any response arrives, the context limit is unknown — in that case, resolution falls through to `execute_threshold_percentage`. Once the first response lands, the correct tokens-based threshold is applied on the following turn.
118+
119+
**When to prefer tokens over percentage:**
120+
121+
- You hit a provider-side prompt cap (like GitHub Copilot's `max_prompt_tokens` ignoring user config overrides — see the github-copilot interaction in the project KNOWN_ISSUES).
122+
- You want consistent compaction behavior across models with very different context window sizes.
123+
124+
**When to prefer percentage:**
125+
126+
- You want the threshold to scale proportionally with the model's window (bigger window → compacts later in absolute terms).
127+
- You're not targeting a specific provider cap.
128+
96129
---
97130

98131
## Model Resolution
@@ -109,6 +142,34 @@ Setting `model` in any agent config overrides the fallback chain entirely. Setti
109142

110143
> **Tip — Dreamer with local models:** Since the dreamer runs during idle time (typically overnight), it works well with local models. Even slower ones like `ollama/mlx-qwen3.5-27b-claude-4.6-opus-reasoning-distilled` are fine — there's no user waiting.
111144
145+
### Advanced agent fields
146+
147+
All three agents (`historian`, `dreamer`, `sidekick`) accept these additional fields beyond the common `model`, `fallback_models`, `temperature`, `variant`, `prompt`. Most map directly to OpenCode's `AgentConfig` and pass through unchanged.
148+
149+
| Field | Type | Description |
150+
|-------|------|-------------|
151+
| `tools` | `{ [toolName: string]: boolean }` | Restrict which tools the agent can use. `{ "bash": false, "write": false }` disables those tools for this agent only. |
152+
| `permission` | `object` | Per-agent permission overrides. Sub-fields: `edit`, `bash`, `webfetch`, `doom_loop`, `external_directory`. Each accepts `"ask"`, `"allow"`, or `"deny"`. `bash` additionally accepts a record form for per-command rules. |
153+
| `disable` | `boolean` | Disable the agent without removing its config. Useful for toggling on/off during testing. |
154+
| `description` | `string` | Agent description shown in OpenCode UI. |
155+
| `mode` | `"subagent"` \| `"primary"` \| `"all"` | OpenCode agent mode. Magic Context internal agents run as `subagent`. |
156+
| `top_p` | `number` (0–1) | Nucleus sampling. |
157+
| `maxSteps` | `number` | Max reasoning steps per agent call. |
158+
| `maxTokens` | `number` | Max output tokens. ⚠️ OpenCode does not currently consume this field for plugin-registered agents — setting it has no effect. Tracked in the project as a known limitation. |
159+
| `color` | `string` (`#RRGGBB`) | Display color in OpenCode UI. |
160+
161+
Example — restricting historian to read-only tools and denying bash:
162+
163+
```jsonc
164+
{
165+
"historian": {
166+
"model": "github-copilot/gpt-5.4",
167+
"tools": { "bash": false, "write": false, "edit": false },
168+
"permission": { "bash": "deny", "webfetch": "deny" }
169+
}
170+
}
171+
```
172+
112173
---
113174

114175
## `historian`
@@ -201,11 +262,18 @@ Controls semantic search for cross-session memories.
201262

202263
| Field | Type | Default | Description |
203264
|-------|------|---------|-------------|
204-
| `provider` | `"local"` \| `"openai-compatible"` \| `"off"` | `"local"` | `"local"` runs `Xenova/all-MiniLM-L6-v2` in-process. |
265+
| `provider` | `"local"` \| `"openai-compatible"` \| `"off"` | `"local"` | `"local"` runs `Xenova/all-MiniLM-L6-v2` in-process. `"off"` disables semantic ranking entirely — see below. |
205266
| `model` | `string` | `"Xenova/all-MiniLM-L6-v2"` | Embedding model. |
206267
| `endpoint` | `string` || Required for `"openai-compatible"`. |
207268
| `api_key` | `string` || Optional API key for remote endpoints. |
208269

270+
When `provider: "off"`:
271+
272+
- No embeddings are generated. `ctx_memory(write)` skips embedding inline and the background embedding sweep becomes a no-op.
273+
- `ctx_search` and memory injection fall back to FTS5 (BM25) ranking only. Keyword matches still work; semantic similarity does not.
274+
- Session-start memory injection still happens when `memory.enabled` is `true` — memories are ordered by utility tier plus `seen_count` rather than semantic similarity to the current turn.
275+
- Memories written while `off` is active will have no embedding row; if you later re-enable `"local"` or `"openai-compatible"`, the background sweep embeds them on the next 15-minute tick.
276+
209277
```jsonc
210278
{
211279
"embedding": {
@@ -256,13 +324,17 @@ It is useful when starting a new session. It's better to choose a fast and cheap
256324
| `fallback_models` | `string` or `string[]` | Fallback models. |
257325
| `temperature` | `number` (0–2) | Sampling temperature. |
258326
| `variant` | `string` | Agent variant. |
259-
| `prompt` | `string` | Agent prompt override. |
327+
| `prompt` | `string` | Persistent agent-level system prompt override. Applies to every sidekick run. |
328+
329+
### Operational fields
260330

261331
| Field | Type | Default | Description |
262332
|-------|------|---------|-------------|
263333
| `enabled` | `boolean` | `false` | Enable sidekick. |
264334
| `timeout_ms` | `number` | `30000` | Timeout per run (ms). |
265-
| `system_prompt` | `string` || Per-run system prompt override for the sidekick child session. |
335+
| `system_prompt` | `string` || Per-invocation system prompt prepended to the sidekick child session for this `/ctx-aug` call only. Layered on top of `prompt` if both are set. |
336+
337+
> **`prompt` vs `system_prompt`:** `prompt` is the persistent agent definition applied to every sidekick run. `system_prompt` is a per-call override injected into that specific child session — useful when a single `/ctx-aug` invocation needs different guidance than the default.
266338
267339
---
268340

@@ -323,6 +395,7 @@ When enabled, dreamer analyzes which files each session's agent reads most frequ
323395
},
324396
"protected_tags": 10,
325397
"auto_drop_tool_age": 50,
398+
"drop_tool_structure": true,
326399
"history_budget_percentage": 0.15,
327400
"compaction_markers": true,
328401

assets/magic-context.schema.json

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@
4848
"oneOf": [
4949
{
5050
"type": "number",
51-
"minimum": 35,
51+
"minimum": 20,
5252
"maximum": 80
5353
},
5454
{
5555
"type": "object",
5656
"properties": {
5757
"default": {
5858
"type": "number",
59-
"minimum": 35,
59+
"minimum": 20,
6060
"maximum": 80
6161
}
6262
},
@@ -65,7 +65,7 @@
6565
],
6666
"additionalProperties": {
6767
"type": "number",
68-
"minimum": 35,
68+
"minimum": 20,
6969
"maximum": 80
7070
},
7171
"description": "Per-model threshold (e.g. { \"default\": 65, \"anthropic/claude-sonnet-4-6\": 40 })"
@@ -74,6 +74,22 @@
7474
"default": 65,
7575
"description": "Context usage percentage that forces queued operations to execute. Capped at 80% for cache safety. Number or per-model object."
7676
},
77+
"execute_threshold_tokens": {
78+
"type": "object",
79+
"properties": {
80+
"default": {
81+
"type": "number",
82+
"minimum": 5000,
83+
"maximum": 2000000
84+
}
85+
},
86+
"additionalProperties": {
87+
"type": "number",
88+
"minimum": 5000,
89+
"maximum": 2000000
90+
},
91+
"description": "Absolute token threshold that forces queued operations to execute. Per-model map (e.g. { \"default\": 150000, \"github-copilot/gpt-5.2-codex\": 40000 }). When set for a model, overrides execute_threshold_percentage. Clamped to 80% × context_limit with a warn log. Requires a resolvable model context limit at runtime to convert to an effective percentage."
92+
},
7793
"nudge_interval_tokens": {
7894
"type": "number",
7995
"minimum": 1000,
@@ -93,6 +109,11 @@
93109
"default": 100,
94110
"description": "Auto-drop tool outputs older than N tags during queue execution"
95111
},
112+
"drop_tool_structure": {
113+
"type": "boolean",
114+
"default": true,
115+
"description": "When true, dropped tool parts are fully removed. When false, tool call structure is preserved (name kept, inputs truncated to 5 chars + '...[truncated]', output replaced with '[truncated]') to prevent agents from hallucinating re-calls."
116+
},
96117
"clear_reasoning_age": {
97118
"type": "number",
98119
"minimum": 10,
@@ -140,6 +161,83 @@
140161
},
141162
"description": "Fire historian when enough commit clusters accumulate in the unsummarized tail"
142163
},
164+
"compaction_markers": {
165+
"type": "boolean",
166+
"default": true,
167+
"description": "Inject compaction boundaries into OpenCode's DB after historian publishes. Reduces transform input size for long sessions by letting OpenCode's filterCompacted skip older messages."
168+
},
169+
"experimental": {
170+
"type": "object",
171+
"properties": {
172+
"user_memories": {
173+
"type": "object",
174+
"properties": {
175+
"enabled": {
176+
"type": "boolean",
177+
"default": false,
178+
"description": "Extract user behavior observations from historian runs and promote recurring patterns to stable user memories injected into all sessions. Requires dreamer."
179+
},
180+
"promotion_threshold": {
181+
"type": "number",
182+
"minimum": 2,
183+
"maximum": 20,
184+
"default": 3,
185+
"description": "Minimum candidate observations before dreamer considers promotion to stable user memory"
186+
}
187+
},
188+
"additionalProperties": false,
189+
"default": {
190+
"enabled": false,
191+
"promotion_threshold": 3
192+
},
193+
"description": "User memory extraction and promotion (experimental)."
194+
},
195+
"pin_key_files": {
196+
"type": "object",
197+
"properties": {
198+
"enabled": {
199+
"type": "boolean",
200+
"default": false,
201+
"description": "Pin frequently-read key files into the system prompt. Dreamer identifies key files per session based on read patterns. Requires dreamer."
202+
},
203+
"token_budget": {
204+
"type": "number",
205+
"minimum": 2000,
206+
"maximum": 30000,
207+
"default": 10000,
208+
"description": "Total token budget for all pinned key files"
209+
},
210+
"min_reads": {
211+
"type": "number",
212+
"minimum": 2,
213+
"maximum": 20,
214+
"default": 4,
215+
"description": "Minimum full-read count before a file is considered for pinning"
216+
}
217+
},
218+
"additionalProperties": false,
219+
"default": {
220+
"enabled": false,
221+
"token_budget": 10000,
222+
"min_reads": 4
223+
},
224+
"description": "Pin frequently-read key files into system prompt (experimental)."
225+
}
226+
},
227+
"additionalProperties": false,
228+
"default": {
229+
"user_memories": {
230+
"enabled": false,
231+
"promotion_threshold": 3
232+
},
233+
"pin_key_files": {
234+
"enabled": false,
235+
"token_budget": 10000,
236+
"min_reads": 4
237+
}
238+
},
239+
"description": "Experimental features — gated behind flags, may change between releases."
240+
},
143241
"historian": {
144242
"type": "object",
145243
"properties": {

packages/dashboard/src/components/ConfigEditor/ConfigEditor.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const FIELD_DEFS: FieldDef[] = [
6464
// General
6565
{ key: "enabled", label: "Enabled", type: "boolean", description: "Enable the magic-context plugin", section: "General" },
6666
{ key: "ctx_reduce_enabled", label: "Agent Controlled Reduction", type: "boolean", description: "Enable agent controlled reductions via ctx_reduce tool. When enabled, agent is prompted and nudged to choose what messages and tool calls to drop periodically. If disabled the system still works via auto drops based on message ages.", section: "General" },
67-
{ key: "drop_tool_structure", label: "Drop Tool Structure", type: "boolean", description: "When enabled, dropped tool calls are fully removed. When disabled, tool input/output is truncated in place so tool structure stays visible.", section: "General", defaultValue: false },
67+
{ key: "drop_tool_structure", label: "Drop Tool Structure", type: "boolean", description: "When enabled, dropped tool calls are fully removed. When disabled, tool input/output is truncated in place so tool structure stays visible.", section: "General", defaultValue: true },
6868
{ key: "compaction_markers", label: "Compaction Markers", type: "boolean", description: "Inject boundary into OpenCode's DB so transform only processes the live tail after historian compaction. Significantly reduces transform input size for long sessions.", section: "General", defaultValue: true },
6969
// Thresholds
7070
// cache_ttl and execute_threshold_percentage are rendered as custom PerModelField components
@@ -81,6 +81,7 @@ const FIELD_DEFS: FieldDef[] = [
8181
{ key: "memory.enabled", label: "Memory Enabled", type: "boolean", description: "Enable cross-session project memory.", section: "Memory" },
8282
{ key: "memory.injection_budget_tokens", label: "Injection Budget (tokens)", type: "number", description: "Max tokens for memory injection into session history.", section: "Memory" },
8383
{ key: "memory.auto_promote", label: "Auto Promote", type: "boolean", description: "Automatically promote session facts to project memory.", section: "Memory" },
84+
{ key: "memory.retrieval_count_promotion_threshold", label: "Retrieval Count Promotion Threshold", type: "number", description: "Minimum ctx_search retrieval count before a session fact is auto-promoted to project memory.", section: "Memory" },
8485
];
8586

8687
// ── Nested value access helpers ─────────────────────────────
@@ -205,7 +206,7 @@ function ConfigForm(props: {
205206
const getRangeConfig = (fieldKey: string): { min: number; max: number; step: number; suffix: string; defaultValue: number } => {
206207
switch (fieldKey) {
207208
case "execute_threshold_percentage":
208-
return { min: 35, max: 80, step: 1, suffix: "%", defaultValue: 65 };
209+
return { min: 20, max: 80, step: 1, suffix: "%", defaultValue: 65 };
209210
case "history_budget_percentage":
210211
return { min: 0.05, max: 0.5, step: 0.01, suffix: "", defaultValue: 0.15 };
211212
case "nudge_interval_tokens":
@@ -592,14 +593,26 @@ function ConfigForm(props: {
592593
<PerModelField
593594
label="Execute Threshold %"
594595
configKey="execute_threshold_percentage"
595-
description="Context usage percentage (35–80) at which queued drops execute. Max 80."
596+
description="Context usage percentage (20–80) at which queued drops execute. Max 80."
596597
value={getNestedValue(formData(), "execute_threshold_percentage") ?? getNestedValue(parsed(), "execute_threshold_percentage")}
597598
onChange={(v) => handleFieldChange("execute_threshold_percentage", v)}
598599
models={models() ?? []}
599600
inputType="slider"
600-
sliderConfig={{ min: 35, max: 80, step: 1, suffix: "%", defaultValue: 65 }}
601+
sliderConfig={{ min: 20, max: 80, step: 1, suffix: "%", defaultValue: 65 }}
601602
defaultPlaceholder="65"
602603
/>
604+
<PerModelField
605+
label="Execute Threshold (tokens)"
606+
configKey="execute_threshold_tokens"
607+
description="Optional absolute-tokens threshold. When set for a model, overrides the percentage above. Per-model map only (use 'default' key for a fallback across all unlisted models). Clamped to 80% × context_limit at runtime."
608+
value={getNestedValue(formData(), "execute_threshold_tokens") ?? getNestedValue(parsed(), "execute_threshold_tokens")}
609+
onChange={(v) => handleFieldChange("execute_threshold_tokens", v)}
610+
models={models() ?? []}
611+
inputType="text"
612+
alwaysObject
613+
numericText
614+
defaultPlaceholder="150000"
615+
/>
603616
</div>
604617
</div>
605618
)}

0 commit comments

Comments
 (0)