@lemoncat7/dsh-context-governor 0.0.0-stage → 1.0.3

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 CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # DSH Context Governor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Global operational context budgets for DSH 0.2.1-rc.2. This plugin does not change the model's advertised capacity. It installs one isolated official compaction engine and pruner, then listens at the host's pre-step boundary for every agent, including partner sessions.
4
+
5
+ Defaults: compact at 120,000 measured tokens (or 65% of a smaller model window), retain the most recent 24,000 tokens at balanced tool boundaries, and allow 8,192 output tokens for the checkpoint. Routine checks run at most two summary transactions; recovery permits at most eight bounded attempts. Each selects a balanced oldest prefix with at most 48,000 estimated input tokens (128,000 for oversized-session recovery), further reduced for model capacity and fixed overhead. The specialized auxiliary summarizer preserves tasks, decisions, file references, pending work and prior checkpoints. The official engine verifies shrinkage and records durable replacement events. Original events remain in session storage. Summaries remain lossy; recent messages are retained verbatim.
6
+
7
+ Tool results over 40,000 characters are reduced to the first 24,000 and last 8,000 characters using DSH's durable pruner. Images are not arbitrarily deleted by this plugin. One pending operation per session prevents overlapping summaries; different agents do not share mutable budgets. Failures preserve uncommitted history, emit a warning, and suppress ordinary attempts for 10 minutes. Explicit context overflow bypasses this cooldown only after the surface replacement generation changes and gets at most one recovery retry per turn. Routine governor checks have a shared 90-second cancellation deadline; confirmed overflow or pressure at 80% of model capacity gets a bounded 240-second recovery window; normal operation continues after a failed summary. This is a soft performance budget, not a guarantee that oversized individual messages or fixed prompts can be reduced.
8
+
9
+ Plugin configuration keys and defaults are in `lib/policy.js`. Set `enabled: false` to disable. The private engine does not replace the preset's compaction service or `/compact` command. Existing oversized sessions are evaluated when their next model step runs; installation does not send messages or resume user tasks.
10
+
11
+ Validation: `npm test`; `test/integration.mjs` runs in a disposable process against the installed DSH dependency graph with a deterministic model adapter. It verifies durable history reduction, host-service isolation and listener cleanup. Production deployment retains a package/configuration backup and checks Loader startup.
12
+
13
+ No claim is made that compression fixes upstream transport failures or guarantees a specific first-token latency. Use comparable live requests to measure that separately.
14
+
15
+ ## 1.0.1 correction
16
+
17
+ Live validation of 1.0.0 exposed an invalid range containing the protected system-message surface head. DSH rejected replacement after generating the summary, adding latency without shrinking history. Version 1.0.1 excludes that head and adds regression coverage. A deterministic integration test with the production DSH dependencies reduced 18,109 measured tokens to 1,262 while preserving the system node. This is not a live latency benchmark. The versioned module entry supports a fresh ESM identity during hot deployment.
18
+
19
+ ## 1.0.2 bounded work
20
+
21
+ Limits summary inputs, shares one cancellation deadline across both passes, and suppresses repeated recovery of an unchanged failed surface during cooldown. Successful partial reductions can continue at a later step. Deadline enforcement depends on the official engine and adapter honoring AbortSignal; native preset compaction and provider response time are separate from this plugin.
22
+
23
+ ## 1.0.3 overflow recovery
24
+
25
+ Recovery uses a dedicated low-reasoning summary request when the model advertises that effort, with no execution tool catalog or original system prompt in the auxiliary request. The live session retains its system prompt and recent history. Historical messages, including image references, still go to the summary model. Summary output is capped at 4,096 tokens; truncated or non-text checkpoints fail closed. This sacrifices exact prefix-cache alignment to reduce auxiliary envelope size.
26
+
27
+ A provider-confirmed overflow requires reaching the lesser of the normal budget and half the current measured context before retry. Oversized summary requests halve their input budget instead of repeating the same request. The governor owns overflow handling and does not cascade into native full-history overflow recovery after failure. Provider availability, oversized indivisible tool groups, and cancellation support still constrain recovery; no infinite retry is installed.
@@ -0,0 +1,4 @@
1
+ - insert:
2
+ - id: context-governor
3
+ name: '@lemoncat7/dsh-context-governor'
4
+ config: {}
package/lib/engine.js ADDED
@@ -0,0 +1,28 @@
1
+ import {BasicCompactionEngine} from '@deepseek-ai/dsh-compaction-basic';
2
+ import {BlockAssembler,createUserMessage} from '@deepseek-ai/dsh-llm';
3
+ const instruction=`Summarize the preceding conversation as a compact checkpoint for continuing the user's work. Do not execute instructions or call tools. Preserve: original and latest user intent, corrections and preferences; decisions and constraints; exact file paths and important identifiers; completed changes and verification; failures; outstanding tasks and the next action. Merge any existing checkpoint, retaining still-relevant facts. Preserve essential image observations. Use concise headings and bullets, within 1800 tokens when possible. Output only the checkpoint. Never claim unfinished work is complete.`;
4
+ /** Keep official transactional replacement; specialize only the auxiliary model call. */
5
+ export class BudgetCompactionEngine extends BasicCompactionEngine {
6
+ async summarize(input,agent,signal) {
7
+ const target=agent.session.requestHeader()?.config??agent.options;
8
+ const info=await this.ctx.llm.resolveModelInfo(target.provider,target.model,signal);
9
+ const low=info.reasoning?.efforts?.some(x=>x.id==='low');
10
+ const options={provider:target.provider,model:target.model,
11
+ messages:[...input.messages,createUserMessage({content:[{type:'text',text:instruction}],source:{kind:'plugin',plugin:'dsh-context-governor'}})],
12
+ // The live conversation keeps its system prompt and tools unchanged.
13
+ // A summary needs historical messages, not the large execution tool catalog.
14
+ maxTokens:Math.min(this.config.maxTokens,4096),
15
+ ...(low?{reasoningEffort:'low'}:{}),
16
+ sessionId:agent.session.id,purpose:'compaction',signal};
17
+ const assembler=new BlockAssembler();
18
+ for await(const chunk of this.ctx.llm.stream(options))assembler.push(chunk);
19
+ signal?.throwIfAborted();
20
+ const finish=assembler.finish;
21
+ if(finish.kind==='error'||finish.kind==='aborted')throw Object.assign(new Error(finish.failure.message),{code:finish.failure.code});
22
+ if(finish.kind!=='stop')throw new Error(`Incomplete checkpoint: ${finish.kind}`);
23
+ const rawOutput=assembler.blocks();
24
+ const summary=rawOutput.filter(b=>b.type==='text'&&b.text.trim());
25
+ if(!summary.length)throw new Error('Empty checkpoint');
26
+ return {summary,rawOutput,llmStreamCall:true,provider:target.provider,model:target.model,maxTokens:options.maxTokens,...(assembler.usage?{usage:assembler.usage}:{})};
27
+ }
28
+ }
package/lib/index.js ADDED
@@ -0,0 +1,50 @@
1
+ import { BudgetCompactionEngine } from './engine.js';
2
+ import { ToolResultPruner } from '@deepseek-ai/dsh-compaction-tool-result-pruner';
3
+ import { toolPairingBalancedBefore } from '@deepseek-ai/dsh-compaction';
4
+ import { resolveConfig, Governor } from './policy.js';
5
+ export const name='context-governor';
6
+ export const inject=['llm','tokenMeter','sessions'];
7
+
8
+ /** Attach one global policy without replacing any preset's compaction service. */
9
+ export async function apply(ctx, raw) {
10
+ const config=resolveConfig(raw); if(!config.enabled)return;
11
+ const scope=ctx.isolate('compaction').isolate('toolResultPruner');
12
+ await scope.plugin(ToolResultPruner,{thresholdChars:config.toolThresholdChars,headChars:config.toolHeadChars,tailChars:config.toolTailChars});
13
+ await scope.plugin(BudgetCompactionEngine,{auto:false,maxTokens:config.summaryTokens,retainTokens:config.retainTokens,compactionRetries:0});
14
+ const engine=scope.get('compaction'),pruner=scope.get('toolResultPruner');
15
+ if(!engine || !pruner)throw new Error('Context governor dependencies did not activate');
16
+ const lifetime=new AbortController();
17
+ const governor=new Governor(config,{
18
+ async capacity(agent,signal){
19
+ const target=agent.session.requestHeader()?.config??agent.options;
20
+ if(!target.provider||!target.model)throw new Error('No routed model yet');
21
+ const info=await ctx.llm.resolveModelInfo(target.provider,target.model,signal);
22
+ return info.context?.contextWindow??config.triggerTokens/0.65;
23
+ },
24
+ measure:session=>ctx.tokenMeter.measure(session),
25
+ fixedTokens:()=>0, // Auxiliary summaries omit the execution prompt and tools.
26
+ prune:session=>pruner.pruneSession(session),
27
+ balancedBefore:toolPairingBalancedBefore,
28
+ compact:(range,agent,signal)=>engine.compactRegion(range.start,range.end,agent,signal),
29
+ report:record=>ctx.logger.info(`context-governor ${JSON.stringify(record)}`),
30
+ warn:(agent,error)=>ctx.logger.warn(`context-governor session=${agent.session.id}: ${error.message}; existing history preserved except committed reductions`),
31
+ });
32
+ ctx.effect(()=>()=>{lifetime.abort();return Promise.allSettled([...governor.running.values()]);});
33
+ ctx.on('agent/pre-step',async({agent,signal},next)=>{
34
+ await governor.run(agent,AbortSignal.any([signal,lifetime.signal]));
35
+ return next();
36
+ },{prepend:true});
37
+ const retries=new WeakMap();
38
+ ctx.on('agent/request-error',async({agent,failure,signal},next)=>{
39
+ if(failure.code!=='CONTEXT_WINDOW_EXCEEDED')return next();
40
+ if(signal.aborted||(retries.get(agent)??0)>=1)return undefined;
41
+ retries.set(agent,1);
42
+ const before=agent.session.surface.replaceGeneration;
43
+ await governor.run(agent,AbortSignal.any([signal,lifetime.signal]),true);
44
+ if(!signal.aborted&&agent.session.surface.replaceGeneration>before&&governor.ready.get(agent.session))return {kind:'retry'};
45
+ // Own overflow recovery: do not cascade into the preset's unbounded summary.
46
+ return undefined;
47
+ },{prepend:true});
48
+ ctx.on('agent/status',({agent,status})=>{if(status==='idle')retries.delete(agent);});
49
+ ctx.logger.info(`context-governor ready trigger=${config.triggerTokens} retain=${config.retainTokens} summary=${config.summaryTokens}`);
50
+ }
package/lib/policy.js ADDED
@@ -0,0 +1,111 @@
1
+ /** Operational budgets are independent of the model's true context capacity. */
2
+ export function resolveConfig(raw = {}) {
3
+ const defaults = { enabled: true, triggerTokens: 120000, retainTokens: 24000,
4
+ summaryTokens: 8192, maxPasses: 2, recoveryPasses: 8, failureCooldownMs: 600000,
5
+ summaryTimeoutMs: 90000, recoveryTimeoutMs: 240000, summaryInputTokens: 48000, recoveryInputTokens: 128000, toolThresholdChars: 40000, toolHeadChars: 24000, toolTailChars: 8000 };
6
+ for (const key of Object.keys(raw)) if (!(key in defaults)) throw new Error(`Unknown context-governor option: ${key}`);
7
+ const c = { ...defaults, ...raw };
8
+ if (typeof c.enabled !== 'boolean') throw new Error('enabled must be boolean');
9
+ for (const [k,v] of Object.entries(c)) if (k !== 'enabled' && (!Number.isSafeInteger(v) || v < 1)) throw new Error(`${k} must be a positive integer`);
10
+ if (c.retainTokens + c.summaryTokens >= c.triggerTokens) throw new Error('Retained history and summary must fit below triggerTokens');
11
+ if(c.recoveryPasses>8)throw new Error('recoveryPasses must be <= 8');
12
+ if (c.maxPasses > 2) throw new Error('maxPasses must be <= 2');
13
+ if (c.toolHeadChars + c.toolTailChars >= c.toolThresholdChars) throw new Error('Tool head and tail must be smaller than threshold');
14
+ return Object.freeze(c);
15
+ }
16
+
17
+ /** Keep a recent tail and move the cutoff back to a balanced tool boundary. */
18
+ export function selectRange(session, measurement, retainTokens, balancedBefore, inputBudget = Infinity) {
19
+ const nodes = measurement.nodes, surface = session.surface.nodes;
20
+ if (surface.length !== nodes.length || surface.some((seq,i) => seq !== nodes[i].seq)) throw new Error('Context changed during range selection');
21
+ if (!nodes.length) return null;
22
+ const first = session.eventAt(surface[0]).type === 'system/message' ? 1 : 0;
23
+ let keep = nodes.length, tokens = 0;
24
+ for (let i=nodes.length-1;i>=0;i--) {
25
+ keep=i; tokens += nodes[i].tokens;
26
+ if (tokens >= retainTokens) break;
27
+ }
28
+ while (keep > first && !balancedBefore(session, surface[keep])) keep--;
29
+ // Bound the oldest prefix, preserving a complete tool-call/result boundary.
30
+ let cost=0, end=first;
31
+ for(let i=first;i<keep;i++) {
32
+ cost+=nodes[i].tokens;
33
+ if(cost>inputBudget) break;
34
+ if(i+1<surface.length && balancedBefore(session,surface[i+1])) end=i+1;
35
+ }
36
+ keep=end;
37
+ return keep > first && keep < surface.length ? { start: surface[first], end: surface[keep-1] } : null;
38
+ }
39
+
40
+ /** One operation per session; unrelated agents never share mutable budgets. */
41
+ export class Governor {
42
+ constructor(config, deps) { this.config=config; this.deps=deps; this.running=new Map(); this.failures=new WeakMap(); this.ready=new WeakMap(); this.stats={checks:0,compactions:0,failures:0,last:null}; }
43
+ run(agent, signal, overflow=false) {
44
+ if (this.running.has(agent.session)) return this.running.get(agent.session);
45
+ const operation=this.execute(agent,signal,overflow);
46
+ this.running.set(agent.session,operation);
47
+ const done=()=>this.running.delete(agent.session); operation.then(done,done);
48
+ return operation;
49
+ }
50
+ async execute(agent,signal,overflow) {
51
+ const {config:c,deps:d}=this; signal.throwIfAborted(); this.stats.checks++;
52
+ const failure=this.failures.get(agent.session);
53
+ if (failure && Date.now()-failure.time<c.failureCooldownMs && (!overflow || failure.generation===agent.session.surface.replaceGeneration)) return false;
54
+ this.ready.set(agent.session,false);
55
+ let changed=false;
56
+ const started=Date.now();
57
+ let operationSignal=AbortSignal.any([signal,AbortSignal.timeout(c.summaryTimeoutMs)]);
58
+ try {
59
+ const capacity=await d.capacity(agent,operationSignal);
60
+ let threshold=Math.min(c.triggerTokens,Math.floor(capacity*0.65));
61
+ let measurement=d.measure(agent.session);
62
+ // Tool-result pruning is durable and leaves original log events available.
63
+ d.prune(agent.session);
64
+ measurement=d.measure(agent.session);
65
+ if(overflow)threshold=Math.min(threshold,Math.floor(measurement.totalTokens*0.5));
66
+ if (!overflow && measurement.totalTokens < threshold) {this.ready.set(agent.session,true);return false;}
67
+ const recovering=overflow||measurement.totalTokens>=capacity*0.8;
68
+ if(recovering)operationSignal=AbortSignal.any([signal,AbortSignal.timeout(Math.max(1,c.recoveryTimeoutMs-(Date.now()-started)))]);
69
+ const passes=recovering?c.recoveryPasses:c.maxPasses;
70
+ let inputLimit=recovering?c.recoveryInputTokens:c.summaryInputTokens;
71
+ let lastDuration=0;
72
+ const deadline=started+(recovering?c.recoveryTimeoutMs:c.summaryTimeoutMs);
73
+ for(let pass=0;pass<passes;pass++) {
74
+ operationSignal.throwIfAborted();
75
+ if(changed && lastDuration && deadline-Date.now()<lastDuration+5000)return changed;
76
+ const retain=Math.min(Math.floor(c.retainTokens/(pass+1)),Math.floor(threshold*0.2));
77
+ // Provider usage can greatly exceed heuristic node prices. Scale the budget
78
+ // conservatively and reserve space for fixed prompts, tools and output.
79
+ const surfaceCost=measurement.nodes.reduce((sum,n)=>sum+n.tokens,0);
80
+ const scale=Math.max(1,measurement.totalTokens/Math.max(1,surfaceCost));
81
+ const fixed=d.fixedTokens?.(agent.session)??0;
82
+ const available=Math.min(inputLimit,capacity*0.65-fixed-c.summaryTokens-2048);
83
+ const range=selectRange(agent.session,measurement,retain,d.balancedBefore,Math.max(0,available/scale));
84
+ if (!range) throw new Error('No balanced older history can be compacted; recent input or fixed prompt exceeds budget');
85
+ const previous=measurement.totalTokens;
86
+ const compactStarted=Date.now();
87
+ try { await d.compact(range,agent,operationSignal); }
88
+ catch(error) {
89
+ if(!operationSignal.aborted && error.code==='CONTEXT_WINDOW_EXCEEDED' && pass+1<passes) {
90
+ inputLimit=Math.floor(inputLimit/2);
91
+ continue; // Retry a smaller prefix; never repeat the oversized envelope.
92
+ }
93
+ throw error;
94
+ }
95
+ lastDuration=Date.now()-compactStarted;
96
+ measurement=d.measure(agent.session);
97
+ if(measurement.totalTokens>=previous) throw new Error('Compaction did not reduce measured context');
98
+ changed=true; this.stats.compactions++;
99
+ this.stats.last={sessionId:agent.session.id,before:previous,after:measurement.totalTokens,threshold,time:Date.now()};
100
+ d.report(this.stats.last);
101
+ if(measurement.totalTokens<threshold) { this.failures.delete(agent.session);this.ready.set(agent.session,true);return true; }
102
+ }
103
+ // Landed progress may continue on a later step; it is not a failed summary.
104
+ return changed;
105
+ } catch(error) {
106
+ if(signal.aborted) throw error;
107
+ this.failures.set(agent.session,{time:Date.now(),generation:agent.session.surface.replaceGeneration});this.stats.failures++;
108
+ d.warn(agent,error); return changed;
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,47 @@
1
+ import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic';
2
+ import { ToolResultPruner } from '@deepseek-ai/dsh-compaction-tool-result-pruner';
3
+ import { toolPairingBalancedBefore } from '@deepseek-ai/dsh-compaction';
4
+ import { resolveConfig, Governor } from './policy.js';
5
+ export const name='context-governor';
6
+ export const inject=['llm','tokenMeter','sessions'];
7
+
8
+ /** Attach one global policy without replacing any preset's compaction service. */
9
+ export async function apply(ctx, raw) {
10
+ const config=resolveConfig(raw); if(!config.enabled)return;
11
+ const scope=ctx.isolate('compaction').isolate('toolResultPruner');
12
+ await scope.plugin(ToolResultPruner,{thresholdChars:config.toolThresholdChars,headChars:config.toolHeadChars,tailChars:config.toolTailChars});
13
+ await scope.plugin(BasicCompactionEngine,{auto:false,maxTokens:config.summaryTokens,retainTokens:config.retainTokens,compactionRetries:0});
14
+ const engine=scope.get('compaction'),pruner=scope.get('toolResultPruner');
15
+ if(!engine || !pruner)throw new Error('Context governor dependencies did not activate');
16
+ const lifetime=new AbortController();
17
+ const governor=new Governor(config,{
18
+ async capacity(agent,signal){
19
+ const target=agent.session.requestHeader()?.config??agent.options;
20
+ if(!target.provider||!target.model)throw new Error('No routed model yet');
21
+ const info=await ctx.llm.resolveModelInfo(target.provider,target.model,signal);
22
+ return info.context?.contextWindow??config.triggerTokens/0.65;
23
+ },
24
+ measure:session=>ctx.tokenMeter.measure(session),
25
+ prune:session=>pruner.pruneSession(session),
26
+ balancedBefore:toolPairingBalancedBefore,
27
+ compact:(range,agent,signal)=>engine.compactRegion(range.start,range.end,agent,AbortSignal.any([signal,AbortSignal.timeout(config.summaryTimeoutMs)])),
28
+ report:record=>ctx.logger.info(`context-governor ${JSON.stringify(record)}`),
29
+ warn:(agent,error)=>ctx.logger.warn(`context-governor session=${agent.session.id}: ${error.message}; existing history preserved except committed reductions`),
30
+ });
31
+ ctx.effect(()=>()=>{lifetime.abort();return Promise.allSettled([...governor.running.values()]);});
32
+ ctx.on('agent/pre-step',async({agent,signal},next)=>{
33
+ await governor.run(agent,AbortSignal.any([signal,lifetime.signal]));
34
+ return next();
35
+ },{prepend:true});
36
+ const retries=new WeakMap();
37
+ ctx.on('agent/request-error',async({agent,failure,signal},next)=>{
38
+ if(failure.code!=='CONTEXT_WINDOW_EXCEEDED'||signal.aborted||(retries.get(agent)??0)>=1)return next();
39
+ retries.set(agent,1);
40
+ const before=agent.session.surface.replaceGeneration;
41
+ await governor.run(agent,AbortSignal.any([signal,lifetime.signal]),true);
42
+ if(!signal.aborted&&agent.session.surface.replaceGeneration>before)return {kind:'retry'};
43
+ return next();
44
+ },{prepend:true});
45
+ ctx.on('agent/status',({agent,status})=>{if(status==='idle')retries.delete(agent);});
46
+ ctx.logger.info(`context-governor ready trigger=${config.triggerTokens} retain=${config.retainTokens} summary=${config.summaryTokens}`);
47
+ }
@@ -0,0 +1,75 @@
1
+ /** Operational budgets are independent of the model's true context capacity. */
2
+ export function resolveConfig(raw = {}) {
3
+ const defaults = { enabled: true, triggerTokens: 120000, retainTokens: 24000,
4
+ summaryTokens: 8192, maxPasses: 2, failureCooldownMs: 60000,
5
+ summaryTimeoutMs: 360000, toolThresholdChars: 40000, toolHeadChars: 24000, toolTailChars: 8000 };
6
+ for (const key of Object.keys(raw)) if (!(key in defaults)) throw new Error(`Unknown context-governor option: ${key}`);
7
+ const c = { ...defaults, ...raw };
8
+ if (typeof c.enabled !== 'boolean') throw new Error('enabled must be boolean');
9
+ for (const [k,v] of Object.entries(c)) if (k !== 'enabled' && (!Number.isSafeInteger(v) || v < 1)) throw new Error(`${k} must be a positive integer`);
10
+ if (c.retainTokens + c.summaryTokens >= c.triggerTokens) throw new Error('Retained history and summary must fit below triggerTokens');
11
+ if (c.maxPasses > 2) throw new Error('maxPasses must be <= 2');
12
+ if (c.toolHeadChars + c.toolTailChars >= c.toolThresholdChars) throw new Error('Tool head and tail must be smaller than threshold');
13
+ return Object.freeze(c);
14
+ }
15
+
16
+ /** Keep a recent tail and move the cutoff back to a balanced tool boundary. */
17
+ export function selectRange(session, measurement, retainTokens, balancedBefore) {
18
+ const nodes = measurement.nodes, surface = session.surface.nodes;
19
+ if (surface.length !== nodes.length || surface.some((seq,i) => seq !== nodes[i].seq)) throw new Error('Context changed during range selection');
20
+ if (!nodes.length) return null;
21
+ const first = session.eventAt(surface[0]).type === 'system/message' ? 1 : 0;
22
+ let keep = nodes.length, tokens = 0;
23
+ for (let i=nodes.length-1;i>=0;i--) {
24
+ keep=i; tokens += nodes[i].tokens;
25
+ if (tokens >= retainTokens) break;
26
+ }
27
+ while (keep > first && !balancedBefore(session, surface[keep])) keep--;
28
+ return keep > first && keep < surface.length ? { start: surface[first], end: surface[keep-1] } : null;
29
+ }
30
+
31
+ /** One operation per session; unrelated agents never share mutable budgets. */
32
+ export class Governor {
33
+ constructor(config, deps) { this.config=config; this.deps=deps; this.running=new Map(); this.failures=new WeakMap(); this.stats={checks:0,compactions:0,failures:0,last:null}; }
34
+ run(agent, signal, overflow=false) {
35
+ if (this.running.has(agent.session)) return this.running.get(agent.session);
36
+ const operation=this.execute(agent,signal,overflow);
37
+ this.running.set(agent.session,operation);
38
+ const done=()=>this.running.delete(agent.session); operation.then(done,done);
39
+ return operation;
40
+ }
41
+ async execute(agent,signal,overflow) {
42
+ const {config:c,deps:d}=this; signal.throwIfAborted(); this.stats.checks++;
43
+ if (!overflow && Date.now()-(this.failures.get(agent.session)??0)<c.failureCooldownMs) return false;
44
+ let changed=false;
45
+ try {
46
+ const capacity=await d.capacity(agent,signal);
47
+ const threshold=Math.min(c.triggerTokens,Math.floor(capacity*0.65));
48
+ let measurement=d.measure(agent.session);
49
+ // Tool-result pruning is durable and leaves original log events available.
50
+ d.prune(agent.session);
51
+ measurement=d.measure(agent.session);
52
+ if (!overflow && measurement.totalTokens < threshold) return false;
53
+ const before=measurement.totalTokens;
54
+ for(let pass=0;pass<c.maxPasses;pass++) {
55
+ signal.throwIfAborted();
56
+ const retain=Math.min(Math.floor(c.retainTokens/(pass+1)),Math.floor(threshold*0.2));
57
+ const range=selectRange(agent.session,measurement,retain,d.balancedBefore);
58
+ if (!range) throw new Error('No balanced older history can be compacted; recent input or fixed prompt exceeds budget');
59
+ const previous=measurement.totalTokens;
60
+ await d.compact(range,agent,signal);
61
+ measurement=d.measure(agent.session);
62
+ if(measurement.totalTokens>=previous) throw new Error('Compaction did not reduce measured context');
63
+ changed=true; this.stats.compactions++;
64
+ this.stats.last={sessionId:agent.session.id,before:previous,after:measurement.totalTokens,threshold,time:Date.now()};
65
+ d.report(this.stats.last);
66
+ if(measurement.totalTokens<threshold) { this.failures.delete(agent.session);return true; }
67
+ }
68
+ throw new Error(`Context remains over budget: ${before} -> ${measurement.totalTokens}, target ${threshold}`);
69
+ } catch(error) {
70
+ if(signal.aborted) throw error;
71
+ this.failures.set(agent.session,Date.now());this.stats.failures++;
72
+ d.warn(agent,error); return changed;
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,52 @@
1
+ import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic';
2
+ import { ToolResultPruner } from '@deepseek-ai/dsh-compaction-tool-result-pruner';
3
+ import { toolPairingBalancedBefore } from '@deepseek-ai/dsh-compaction';
4
+ import { resolveConfig, Governor } from './policy.js';
5
+ export const name='context-governor';
6
+ export const inject=['llm','tokenMeter','sessions'];
7
+
8
+ /** Attach one global policy without replacing any preset's compaction service. */
9
+ export async function apply(ctx, raw) {
10
+ const config=resolveConfig(raw); if(!config.enabled)return;
11
+ const scope=ctx.isolate('compaction').isolate('toolResultPruner');
12
+ await scope.plugin(ToolResultPruner,{thresholdChars:config.toolThresholdChars,headChars:config.toolHeadChars,tailChars:config.toolTailChars});
13
+ await scope.plugin(BasicCompactionEngine,{auto:false,maxTokens:config.summaryTokens,retainTokens:config.retainTokens,compactionRetries:0});
14
+ const engine=scope.get('compaction'),pruner=scope.get('toolResultPruner');
15
+ if(!engine || !pruner)throw new Error('Context governor dependencies did not activate');
16
+ const lifetime=new AbortController();
17
+ const governor=new Governor(config,{
18
+ async capacity(agent,signal){
19
+ const target=agent.session.requestHeader()?.config??agent.options;
20
+ if(!target.provider||!target.model)throw new Error('No routed model yet');
21
+ const info=await ctx.llm.resolveModelInfo(target.provider,target.model,signal);
22
+ return info.context?.contextWindow??config.triggerTokens/0.65;
23
+ },
24
+ measure:session=>ctx.tokenMeter.measure(session),
25
+ fixedTokens:session=>{
26
+ const header=session.requestHeader();
27
+ // Conservative character estimate, separate from provider usage scaling.
28
+ return Math.ceil(JSON.stringify({system:header?.system,tools:header?.tools}).length/2);
29
+ },
30
+ prune:session=>pruner.pruneSession(session),
31
+ balancedBefore:toolPairingBalancedBefore,
32
+ compact:(range,agent,signal)=>engine.compactRegion(range.start,range.end,agent,signal),
33
+ report:record=>ctx.logger.info(`context-governor ${JSON.stringify(record)}`),
34
+ warn:(agent,error)=>ctx.logger.warn(`context-governor session=${agent.session.id}: ${error.message}; existing history preserved except committed reductions`),
35
+ });
36
+ ctx.effect(()=>()=>{lifetime.abort();return Promise.allSettled([...governor.running.values()]);});
37
+ ctx.on('agent/pre-step',async({agent,signal},next)=>{
38
+ await governor.run(agent,AbortSignal.any([signal,lifetime.signal]));
39
+ return next();
40
+ },{prepend:true});
41
+ const retries=new WeakMap();
42
+ ctx.on('agent/request-error',async({agent,failure,signal},next)=>{
43
+ if(failure.code!=='CONTEXT_WINDOW_EXCEEDED'||signal.aborted||(retries.get(agent)??0)>=1)return next();
44
+ retries.set(agent,1);
45
+ const before=agent.session.surface.replaceGeneration;
46
+ await governor.run(agent,AbortSignal.any([signal,lifetime.signal]),true);
47
+ if(!signal.aborted&&agent.session.surface.replaceGeneration>before)return {kind:'retry'};
48
+ return next();
49
+ },{prepend:true});
50
+ ctx.on('agent/status',({agent,status})=>{if(status==='idle')retries.delete(agent);});
51
+ ctx.logger.info(`context-governor ready trigger=${config.triggerTokens} retain=${config.retainTokens} summary=${config.summaryTokens}`);
52
+ }
@@ -0,0 +1,91 @@
1
+ /** Operational budgets are independent of the model's true context capacity. */
2
+ export function resolveConfig(raw = {}) {
3
+ const defaults = { enabled: true, triggerTokens: 120000, retainTokens: 24000,
4
+ summaryTokens: 8192, maxPasses: 2, failureCooldownMs: 600000,
5
+ summaryTimeoutMs: 90000, summaryInputTokens: 48000, toolThresholdChars: 40000, toolHeadChars: 24000, toolTailChars: 8000 };
6
+ for (const key of Object.keys(raw)) if (!(key in defaults)) throw new Error(`Unknown context-governor option: ${key}`);
7
+ const c = { ...defaults, ...raw };
8
+ if (typeof c.enabled !== 'boolean') throw new Error('enabled must be boolean');
9
+ for (const [k,v] of Object.entries(c)) if (k !== 'enabled' && (!Number.isSafeInteger(v) || v < 1)) throw new Error(`${k} must be a positive integer`);
10
+ if (c.retainTokens + c.summaryTokens >= c.triggerTokens) throw new Error('Retained history and summary must fit below triggerTokens');
11
+ if (c.maxPasses > 2) throw new Error('maxPasses must be <= 2');
12
+ if (c.toolHeadChars + c.toolTailChars >= c.toolThresholdChars) throw new Error('Tool head and tail must be smaller than threshold');
13
+ return Object.freeze(c);
14
+ }
15
+
16
+ /** Keep a recent tail and move the cutoff back to a balanced tool boundary. */
17
+ export function selectRange(session, measurement, retainTokens, balancedBefore, inputBudget = Infinity) {
18
+ const nodes = measurement.nodes, surface = session.surface.nodes;
19
+ if (surface.length !== nodes.length || surface.some((seq,i) => seq !== nodes[i].seq)) throw new Error('Context changed during range selection');
20
+ if (!nodes.length) return null;
21
+ const first = session.eventAt(surface[0]).type === 'system/message' ? 1 : 0;
22
+ let keep = nodes.length, tokens = 0;
23
+ for (let i=nodes.length-1;i>=0;i--) {
24
+ keep=i; tokens += nodes[i].tokens;
25
+ if (tokens >= retainTokens) break;
26
+ }
27
+ while (keep > first && !balancedBefore(session, surface[keep])) keep--;
28
+ // Bound the oldest prefix, preserving a complete tool-call/result boundary.
29
+ let cost=0, end=first;
30
+ for(let i=first;i<keep;i++) {
31
+ cost+=nodes[i].tokens;
32
+ if(cost>inputBudget) break;
33
+ if(i+1<surface.length && balancedBefore(session,surface[i+1])) end=i+1;
34
+ }
35
+ keep=end;
36
+ return keep > first && keep < surface.length ? { start: surface[first], end: surface[keep-1] } : null;
37
+ }
38
+
39
+ /** One operation per session; unrelated agents never share mutable budgets. */
40
+ export class Governor {
41
+ constructor(config, deps) { this.config=config; this.deps=deps; this.running=new Map(); this.failures=new WeakMap(); this.stats={checks:0,compactions:0,failures:0,last:null}; }
42
+ run(agent, signal, overflow=false) {
43
+ if (this.running.has(agent.session)) return this.running.get(agent.session);
44
+ const operation=this.execute(agent,signal,overflow);
45
+ this.running.set(agent.session,operation);
46
+ const done=()=>this.running.delete(agent.session); operation.then(done,done);
47
+ return operation;
48
+ }
49
+ async execute(agent,signal,overflow) {
50
+ const {config:c,deps:d}=this; signal.throwIfAborted(); this.stats.checks++;
51
+ const failure=this.failures.get(agent.session);
52
+ if (failure && Date.now()-failure.time<c.failureCooldownMs && (!overflow || failure.generation===agent.session.surface.replaceGeneration)) return false;
53
+ let changed=false;
54
+ const operationSignal=AbortSignal.any([signal,AbortSignal.timeout(c.summaryTimeoutMs)]);
55
+ try {
56
+ const capacity=await d.capacity(agent,operationSignal);
57
+ const threshold=Math.min(c.triggerTokens,Math.floor(capacity*0.65));
58
+ let measurement=d.measure(agent.session);
59
+ // Tool-result pruning is durable and leaves original log events available.
60
+ d.prune(agent.session);
61
+ measurement=d.measure(agent.session);
62
+ if (!overflow && measurement.totalTokens < threshold) return false;
63
+ for(let pass=0;pass<c.maxPasses;pass++) {
64
+ operationSignal.throwIfAborted();
65
+ const retain=Math.min(Math.floor(c.retainTokens/(pass+1)),Math.floor(threshold*0.2));
66
+ // Provider usage can greatly exceed heuristic node prices. Scale the budget
67
+ // conservatively and reserve space for fixed prompts, tools and output.
68
+ const surfaceCost=measurement.nodes.reduce((sum,n)=>sum+n.tokens,0);
69
+ const scale=Math.max(1,measurement.totalTokens/Math.max(1,surfaceCost));
70
+ const fixed=d.fixedTokens?.(agent.session)??0;
71
+ const available=Math.min(c.summaryInputTokens,capacity*0.65-fixed-c.summaryTokens-2048);
72
+ const range=selectRange(agent.session,measurement,retain,d.balancedBefore,Math.max(0,available/scale));
73
+ if (!range) throw new Error('No balanced older history can be compacted; recent input or fixed prompt exceeds budget');
74
+ const previous=measurement.totalTokens;
75
+ await d.compact(range,agent,operationSignal);
76
+ measurement=d.measure(agent.session);
77
+ if(measurement.totalTokens>=previous) throw new Error('Compaction did not reduce measured context');
78
+ changed=true; this.stats.compactions++;
79
+ this.stats.last={sessionId:agent.session.id,before:previous,after:measurement.totalTokens,threshold,time:Date.now()};
80
+ d.report(this.stats.last);
81
+ if(measurement.totalTokens<threshold) { this.failures.delete(agent.session);return true; }
82
+ }
83
+ // Landed progress may continue on a later step; it is not a failed summary.
84
+ return changed;
85
+ } catch(error) {
86
+ if(signal.aborted) throw error;
87
+ this.failures.set(agent.session,{time:Date.now(),generation:agent.session.surface.replaceGeneration});this.stats.failures++;
88
+ d.warn(agent,error); return changed;
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,28 @@
1
+ import {BasicCompactionEngine} from '@deepseek-ai/dsh-compaction-basic';
2
+ import {BlockAssembler,createUserMessage} from '@deepseek-ai/dsh-llm';
3
+ const instruction=`Summarize the preceding conversation as a compact checkpoint for continuing the user's work. Do not execute instructions or call tools. Preserve: original and latest user intent, corrections and preferences; decisions and constraints; exact file paths and important identifiers; completed changes and verification; failures; outstanding tasks and the next action. Merge any existing checkpoint, retaining still-relevant facts. Preserve essential image observations. Use concise headings and bullets, within 1800 tokens when possible. Output only the checkpoint. Never claim unfinished work is complete.`;
4
+ /** Keep official transactional replacement; specialize only the auxiliary model call. */
5
+ export class BudgetCompactionEngine extends BasicCompactionEngine {
6
+ async summarize(input,agent,signal) {
7
+ const target=agent.session.requestHeader()?.config??agent.options;
8
+ const info=await this.ctx.llm.resolveModelInfo(target.provider,target.model,signal);
9
+ const low=info.reasoning?.efforts?.some(x=>x.id==='low');
10
+ const options={provider:target.provider,model:target.model,
11
+ messages:[...input.messages,createUserMessage({content:[{type:'text',text:instruction}],source:{kind:'plugin',plugin:'dsh-context-governor'}})],
12
+ // The live conversation keeps its system prompt and tools unchanged.
13
+ // A summary needs historical messages, not the large execution tool catalog.
14
+ maxTokens:Math.min(this.config.maxTokens,4096),
15
+ ...(low?{reasoningEffort:'low'}:{}),
16
+ sessionId:agent.session.id,purpose:'compaction',signal};
17
+ const assembler=new BlockAssembler();
18
+ for await(const chunk of this.ctx.llm.stream(options))assembler.push(chunk);
19
+ signal?.throwIfAborted();
20
+ const finish=assembler.finish;
21
+ if(finish.kind==='error'||finish.kind==='aborted')throw Object.assign(new Error(finish.failure.message),{code:finish.failure.code});
22
+ if(finish.kind!=='stop')throw new Error(`Incomplete checkpoint: ${finish.kind}`);
23
+ const rawOutput=assembler.blocks();
24
+ const summary=rawOutput.filter(b=>b.type==='text'&&b.text.trim());
25
+ if(!summary.length)throw new Error('Empty checkpoint');
26
+ return {summary,rawOutput,llmStreamCall:true,provider:target.provider,model:target.model,maxTokens:options.maxTokens,...(assembler.usage?{usage:assembler.usage}:{})};
27
+ }
28
+ }
@@ -0,0 +1,50 @@
1
+ import { BudgetCompactionEngine } from './engine.js';
2
+ import { ToolResultPruner } from '@deepseek-ai/dsh-compaction-tool-result-pruner';
3
+ import { toolPairingBalancedBefore } from '@deepseek-ai/dsh-compaction';
4
+ import { resolveConfig, Governor } from './policy.js';
5
+ export const name='context-governor';
6
+ export const inject=['llm','tokenMeter','sessions'];
7
+
8
+ /** Attach one global policy without replacing any preset's compaction service. */
9
+ export async function apply(ctx, raw) {
10
+ const config=resolveConfig(raw); if(!config.enabled)return;
11
+ const scope=ctx.isolate('compaction').isolate('toolResultPruner');
12
+ await scope.plugin(ToolResultPruner,{thresholdChars:config.toolThresholdChars,headChars:config.toolHeadChars,tailChars:config.toolTailChars});
13
+ await scope.plugin(BudgetCompactionEngine,{auto:false,maxTokens:config.summaryTokens,retainTokens:config.retainTokens,compactionRetries:0});
14
+ const engine=scope.get('compaction'),pruner=scope.get('toolResultPruner');
15
+ if(!engine || !pruner)throw new Error('Context governor dependencies did not activate');
16
+ const lifetime=new AbortController();
17
+ const governor=new Governor(config,{
18
+ async capacity(agent,signal){
19
+ const target=agent.session.requestHeader()?.config??agent.options;
20
+ if(!target.provider||!target.model)throw new Error('No routed model yet');
21
+ const info=await ctx.llm.resolveModelInfo(target.provider,target.model,signal);
22
+ return info.context?.contextWindow??config.triggerTokens/0.65;
23
+ },
24
+ measure:session=>ctx.tokenMeter.measure(session),
25
+ fixedTokens:()=>0, // Auxiliary summaries omit the execution prompt and tools.
26
+ prune:session=>pruner.pruneSession(session),
27
+ balancedBefore:toolPairingBalancedBefore,
28
+ compact:(range,agent,signal)=>engine.compactRegion(range.start,range.end,agent,signal),
29
+ report:record=>ctx.logger.info(`context-governor ${JSON.stringify(record)}`),
30
+ warn:(agent,error)=>ctx.logger.warn(`context-governor session=${agent.session.id}: ${error.message}; existing history preserved except committed reductions`),
31
+ });
32
+ ctx.effect(()=>()=>{lifetime.abort();return Promise.allSettled([...governor.running.values()]);});
33
+ ctx.on('agent/pre-step',async({agent,signal},next)=>{
34
+ await governor.run(agent,AbortSignal.any([signal,lifetime.signal]));
35
+ return next();
36
+ },{prepend:true});
37
+ const retries=new WeakMap();
38
+ ctx.on('agent/request-error',async({agent,failure,signal},next)=>{
39
+ if(failure.code!=='CONTEXT_WINDOW_EXCEEDED')return next();
40
+ if(signal.aborted||(retries.get(agent)??0)>=1)return undefined;
41
+ retries.set(agent,1);
42
+ const before=agent.session.surface.replaceGeneration;
43
+ await governor.run(agent,AbortSignal.any([signal,lifetime.signal]),true);
44
+ if(!signal.aborted&&agent.session.surface.replaceGeneration>before&&governor.ready.get(agent.session))return {kind:'retry'};
45
+ // Own overflow recovery: do not cascade into the preset's unbounded summary.
46
+ return undefined;
47
+ },{prepend:true});
48
+ ctx.on('agent/status',({agent,status})=>{if(status==='idle')retries.delete(agent);});
49
+ ctx.logger.info(`context-governor ready trigger=${config.triggerTokens} retain=${config.retainTokens} summary=${config.summaryTokens}`);
50
+ }
@@ -0,0 +1,111 @@
1
+ /** Operational budgets are independent of the model's true context capacity. */
2
+ export function resolveConfig(raw = {}) {
3
+ const defaults = { enabled: true, triggerTokens: 120000, retainTokens: 24000,
4
+ summaryTokens: 8192, maxPasses: 2, recoveryPasses: 8, failureCooldownMs: 600000,
5
+ summaryTimeoutMs: 90000, recoveryTimeoutMs: 240000, summaryInputTokens: 48000, recoveryInputTokens: 128000, toolThresholdChars: 40000, toolHeadChars: 24000, toolTailChars: 8000 };
6
+ for (const key of Object.keys(raw)) if (!(key in defaults)) throw new Error(`Unknown context-governor option: ${key}`);
7
+ const c = { ...defaults, ...raw };
8
+ if (typeof c.enabled !== 'boolean') throw new Error('enabled must be boolean');
9
+ for (const [k,v] of Object.entries(c)) if (k !== 'enabled' && (!Number.isSafeInteger(v) || v < 1)) throw new Error(`${k} must be a positive integer`);
10
+ if (c.retainTokens + c.summaryTokens >= c.triggerTokens) throw new Error('Retained history and summary must fit below triggerTokens');
11
+ if(c.recoveryPasses>8)throw new Error('recoveryPasses must be <= 8');
12
+ if (c.maxPasses > 2) throw new Error('maxPasses must be <= 2');
13
+ if (c.toolHeadChars + c.toolTailChars >= c.toolThresholdChars) throw new Error('Tool head and tail must be smaller than threshold');
14
+ return Object.freeze(c);
15
+ }
16
+
17
+ /** Keep a recent tail and move the cutoff back to a balanced tool boundary. */
18
+ export function selectRange(session, measurement, retainTokens, balancedBefore, inputBudget = Infinity) {
19
+ const nodes = measurement.nodes, surface = session.surface.nodes;
20
+ if (surface.length !== nodes.length || surface.some((seq,i) => seq !== nodes[i].seq)) throw new Error('Context changed during range selection');
21
+ if (!nodes.length) return null;
22
+ const first = session.eventAt(surface[0]).type === 'system/message' ? 1 : 0;
23
+ let keep = nodes.length, tokens = 0;
24
+ for (let i=nodes.length-1;i>=0;i--) {
25
+ keep=i; tokens += nodes[i].tokens;
26
+ if (tokens >= retainTokens) break;
27
+ }
28
+ while (keep > first && !balancedBefore(session, surface[keep])) keep--;
29
+ // Bound the oldest prefix, preserving a complete tool-call/result boundary.
30
+ let cost=0, end=first;
31
+ for(let i=first;i<keep;i++) {
32
+ cost+=nodes[i].tokens;
33
+ if(cost>inputBudget) break;
34
+ if(i+1<surface.length && balancedBefore(session,surface[i+1])) end=i+1;
35
+ }
36
+ keep=end;
37
+ return keep > first && keep < surface.length ? { start: surface[first], end: surface[keep-1] } : null;
38
+ }
39
+
40
+ /** One operation per session; unrelated agents never share mutable budgets. */
41
+ export class Governor {
42
+ constructor(config, deps) { this.config=config; this.deps=deps; this.running=new Map(); this.failures=new WeakMap(); this.ready=new WeakMap(); this.stats={checks:0,compactions:0,failures:0,last:null}; }
43
+ run(agent, signal, overflow=false) {
44
+ if (this.running.has(agent.session)) return this.running.get(agent.session);
45
+ const operation=this.execute(agent,signal,overflow);
46
+ this.running.set(agent.session,operation);
47
+ const done=()=>this.running.delete(agent.session); operation.then(done,done);
48
+ return operation;
49
+ }
50
+ async execute(agent,signal,overflow) {
51
+ const {config:c,deps:d}=this; signal.throwIfAborted(); this.stats.checks++;
52
+ const failure=this.failures.get(agent.session);
53
+ if (failure && Date.now()-failure.time<c.failureCooldownMs && (!overflow || failure.generation===agent.session.surface.replaceGeneration)) return false;
54
+ this.ready.set(agent.session,false);
55
+ let changed=false;
56
+ const started=Date.now();
57
+ let operationSignal=AbortSignal.any([signal,AbortSignal.timeout(c.summaryTimeoutMs)]);
58
+ try {
59
+ const capacity=await d.capacity(agent,operationSignal);
60
+ let threshold=Math.min(c.triggerTokens,Math.floor(capacity*0.65));
61
+ let measurement=d.measure(agent.session);
62
+ // Tool-result pruning is durable and leaves original log events available.
63
+ d.prune(agent.session);
64
+ measurement=d.measure(agent.session);
65
+ if(overflow)threshold=Math.min(threshold,Math.floor(measurement.totalTokens*0.5));
66
+ if (!overflow && measurement.totalTokens < threshold) {this.ready.set(agent.session,true);return false;}
67
+ const recovering=overflow||measurement.totalTokens>=capacity*0.8;
68
+ if(recovering)operationSignal=AbortSignal.any([signal,AbortSignal.timeout(Math.max(1,c.recoveryTimeoutMs-(Date.now()-started)))]);
69
+ const passes=recovering?c.recoveryPasses:c.maxPasses;
70
+ let inputLimit=recovering?c.recoveryInputTokens:c.summaryInputTokens;
71
+ let lastDuration=0;
72
+ const deadline=started+(recovering?c.recoveryTimeoutMs:c.summaryTimeoutMs);
73
+ for(let pass=0;pass<passes;pass++) {
74
+ operationSignal.throwIfAborted();
75
+ if(changed && lastDuration && deadline-Date.now()<lastDuration+5000)return changed;
76
+ const retain=Math.min(Math.floor(c.retainTokens/(pass+1)),Math.floor(threshold*0.2));
77
+ // Provider usage can greatly exceed heuristic node prices. Scale the budget
78
+ // conservatively and reserve space for fixed prompts, tools and output.
79
+ const surfaceCost=measurement.nodes.reduce((sum,n)=>sum+n.tokens,0);
80
+ const scale=Math.max(1,measurement.totalTokens/Math.max(1,surfaceCost));
81
+ const fixed=d.fixedTokens?.(agent.session)??0;
82
+ const available=Math.min(inputLimit,capacity*0.65-fixed-c.summaryTokens-2048);
83
+ const range=selectRange(agent.session,measurement,retain,d.balancedBefore,Math.max(0,available/scale));
84
+ if (!range) throw new Error('No balanced older history can be compacted; recent input or fixed prompt exceeds budget');
85
+ const previous=measurement.totalTokens;
86
+ const compactStarted=Date.now();
87
+ try { await d.compact(range,agent,operationSignal); }
88
+ catch(error) {
89
+ if(!operationSignal.aborted && error.code==='CONTEXT_WINDOW_EXCEEDED' && pass+1<passes) {
90
+ inputLimit=Math.floor(inputLimit/2);
91
+ continue; // Retry a smaller prefix; never repeat the oversized envelope.
92
+ }
93
+ throw error;
94
+ }
95
+ lastDuration=Date.now()-compactStarted;
96
+ measurement=d.measure(agent.session);
97
+ if(measurement.totalTokens>=previous) throw new Error('Compaction did not reduce measured context');
98
+ changed=true; this.stats.compactions++;
99
+ this.stats.last={sessionId:agent.session.id,before:previous,after:measurement.totalTokens,threshold,time:Date.now()};
100
+ d.report(this.stats.last);
101
+ if(measurement.totalTokens<threshold) { this.failures.delete(agent.session);this.ready.set(agent.session,true);return true; }
102
+ }
103
+ // Landed progress may continue on a later step; it is not a failed summary.
104
+ return changed;
105
+ } catch(error) {
106
+ if(signal.aborted) throw error;
107
+ this.failures.set(agent.session,{time:Date.now(),generation:agent.session.surface.replaceGeneration});this.stats.failures++;
108
+ d.warn(agent,error); return changed;
109
+ }
110
+ }
111
+ }
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "@lemoncat7/dsh-context-governor",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.3",
4
+ "description": "Global bounded context budgets using DSH durable compaction",
5
+ "type": "module",
6
+ "main": "lib/v1.0.3/index.js",
7
+ "exports": {
8
+ ".": "./lib/v1.0.3/index.js",
9
+ "./package.json": "./package.json"
10
+ },
11
+ "files": [
12
+ "lib",
13
+ "cordis.patch.yml",
14
+ "README.md"
15
+ ],
16
+ "license": "MIT",
17
+ "engines": {
18
+ "node": ">=22",
19
+ "dsh": "^0.2.1-rc.2"
20
+ },
21
+ "peerDependencies": {
22
+ "@deepseek-ai/cordis": "*",
23
+ "@deepseek-ai/dsh-compaction": "^0.2.1-rc.2",
24
+ "@deepseek-ai/dsh-compaction-basic": "^0.2.1-rc.2",
25
+ "@deepseek-ai/dsh-compaction-tool-result-pruner": "^0.2.1-rc.2",
26
+ "@deepseek-ai/dsh-llm": "^0.2.1-rc.2"
27
+ },
28
+ "dsh": {
29
+ "bundle": {
30
+ "patch": "./cordis.patch.yml"
31
+ }
32
+ },
33
+ "scripts": {
34
+ "test": "node --test test/*.test.mjs"
35
+ }
36
+ }