dsh-strata-sync 0.10.3 → 0.10.4

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
@@ -69,7 +69,7 @@ For example, with automatic output calculation, a 32K context uses a 9,830-token
69
69
 
70
70
  ## Summary prompt and target length
71
71
 
72
- **Dynamic summary target** is enabled by default. It supplements DSH's native summary instructions; disabling it keeps native summary and compaction behavior.
72
+ **Dynamic summary target** is off by default. Enable it to supplement DSH's native summary instructions. When off, DSH uses its native summary and compaction behavior.
73
73
 
74
74
  The editable **Supplemental summary prompt** starts with:
75
75
 
@@ -79,17 +79,20 @@ Keep the entire checkpoint, including all required headings, within a target of
79
79
 
80
80
  Keep `{target}` in your text—it is replaced with the calculated token target before each supported summary. The template can contain up to 2,048 characters. Use **Restore default prompt** to reset it, then **Save preferences** to apply your choice.
81
81
 
82
- Expand **Calculation and limits** for the additional ratios:
82
+ Two divisors control the target (both default to **2**, or **50%**):
83
83
 
84
84
  | Divisor | Default | Limits the target to |
85
85
  |---|---:|---|
86
- | Context (N) | 16 | Context ÷ N |
87
- | Summary output (O) | 2 | Actual summary output cap ÷ O |
86
+ | Output budget (O) | 2 | Estimated effective summary output budget ÷ O |
88
87
  | Source length (S) | 2 | Estimated content being summarized ÷ S |
89
88
 
90
- The target is the smallest result, rounded down. All divisors accept decimals of at least 1. Increasing a divisor lowers its bound; another bound may still determine the final target. Context uses the smaller verified server/client window, and the source estimate excludes system prompts and tool definitions.
89
+ The target is the smaller result, rounded down. All divisors accept decimals of at least 1. There is no context divisor; a legacy `summaryContextDivisor` value is ignored. Existing output/source divisors and explicit enable choices are preserved. New or unspecified guidance remains **off**.
91
90
 
92
- With 32K context, a 4,096-token summary cap and enough source content, the defaults target **2,048 tokens**. This is a writing instruction, **not a hard output limit or a guarantee against truncation**. The UI previews a ceiling; the actual request may have a smaller target.
91
+ The effective output budget is an **estimate**, not the raw requested `maxTokens` or a server-confirmed cap. It is the smaller of the request's summary cap and estimated remaining context. The latter uses the smaller verified server/client window, subtracts the full prompt (including system text, tool schemas and the supplement), and allows for the supported pi-ai adapter's 4,096-token safety reserve. Text is estimated at 3.5 characters/token with extra framing. The separate source estimate uses DSH's four-character heuristic and excludes the system prompt, tools and summary instruction. Neither estimate is an exact tokenizer count.
92
+
93
+ For example, a 4,096-token effective output budget with enough source content gives a **2,048-token target** in either 32K or 64K context. A 2,000-token source instead gives a **1,000-token target**. Less remaining context can reduce the target further. The UI previews an upper bound from the proposed summary cap, before request-specific costs are known.
94
+
95
+ This is a writing instruction, **not a hard output limit or a guarantee against truncation**. Reasoning may consume the same output budget; the plugin does not change reasoning, `maxTokens`, `fit_max_tokens` or compaction timing. The server's actual tokenizer, later middleware, or a different adapter version may impose a lower limit.
93
96
 
94
97
  Saving summary preferences does not change model budgets. Native prompt content is preserved, and no extra inference or retry loop is added. If the request cannot be verified or safely estimated, or the target is below 256 tokens, the original summary request proceeds unchanged.
95
98
 
package/dist/client.js CHANGED
@@ -115,9 +115,9 @@ window.__ModuleLoader__.load({
115
115
  "zh-Hant": "啟用後,在每次支援的 Strata 摘要請求前,計算長度目標,並在 DSH 原生摘要提示詞後補充一句。",
116
116
  "zh-Hans": "启用后,在每次支持的 Strata 摘要请求前,计算长度目标,并在 DSH 原生摘要提示词后补充一句。"
117
117
  },
118
- "Target = floor(min(context / N, summary output limit / O, selected content estimate / S)).": {
119
- "zh-Hant": "目標 = floor(min(上下文 / N, 摘要輸出上限 / O, 待壓縮內容估算 / S))。",
120
- "zh-Hans": "目标 = floor(min(上下文 / N, 摘要输出上限 / O, 待压缩内容估算 / S))。"
118
+ "Target = floor(min(estimated effective output budget / O, selected content estimate / S)). Default divisors: 2 and 2 (50% each).": {
119
+ "zh-Hant": "目標 = floor(min(估算有效輸出預算 / O, 待壓縮內容估算 / S))。兩個除數預設均為 2(各取 50%)。",
120
+ "zh-Hans": "目标 = floor(min(估算有效输出预算 / O, 待压缩内容估算 / S))。两个除数默认均为 2(各取 50%)。"
121
121
  },
122
122
  "When guidance cannot be applied, DSH keeps its native summary behavior.": {
123
123
  "zh-Hant": "無法套用時,沿用 DSH 原生摘要。",
@@ -127,25 +127,21 @@ window.__ModuleLoader__.load({
127
127
  "zh-Hant": "啟用摘要提示補充",
128
128
  "zh-Hans": "启用摘要提示补充"
129
129
  },
130
- "Enabled by default. Turning this off keeps DSH's native summary and automatic compaction behavior.": {
131
- "zh-Hant": "預設開啟。關閉後保留 DSH 原生摘要與自動壓縮行為。",
132
- "zh-Hans": "默认开启。关闭后保留 DSH 原生摘要与自动压缩行为。"
130
+ "Off by default. Enable it to supplement DSH's native summary and automatic compaction behavior.": {
131
+ "zh-Hant": "預設關閉。啟用後補充 DSH 原生摘要提示詞。",
132
+ "zh-Hans": "默认关闭。启用后补充 DSH 原生摘要提示词。"
133
133
  },
134
- "Context divisor (context ÷ N)": {
135
- "zh-Hant": "上下文除數(上下文 ÷ N)",
136
- "zh-Hans": "上下文除数(上下文 ÷ N)"
137
- },
138
- "Summary output divisor (output limit ÷ O)": {
139
- "zh-Hant": "摘要輸出除數(輸出上限 ÷ O)",
140
- "zh-Hans": "摘要输出除数(输出上限 ÷ O)"
134
+ "Output budget divisor (estimated budget ÷ O)": {
135
+ "zh-Hant": "摘要輸出除數(估算可用預算 ÷ O)",
136
+ "zh-Hans": "摘要输出除数(估算可用预算 ÷ O)"
141
137
  },
142
138
  "Source length divisor (source estimate ÷ S)": {
143
139
  "zh-Hant": "原文長度除數(原文估算 ÷ S)",
144
140
  "zh-Hans": "原文长度除数(原文估算 ÷ S)"
145
141
  },
146
- "Defaults: N = 16, O = 2, S = 2. Smaller divisors allow longer targets; the smallest of the three bounds wins. Decimals are supported.": {
147
- "zh-Hant": "預設 N = 16、O = 2、S = 2。除數越小,目標可越長;最後取三項限制的最小值。支援小數。",
148
- "zh-Hans": "默认 N = 16、O = 2、S = 2。除数越小,目标可越长;最后取三项限制的最小值。支持小数。"
142
+ "Defaults: O = 2, S = 2 (50% each). Smaller divisors allow longer targets; the smaller of the two bounds wins. Decimals are supported.": {
143
+ "zh-Hant": "預設 O = 2、S = 2(各取 50%)。除數越小,目標可越長;最後取兩項限制的較小值。支援小數。",
144
+ "zh-Hans": "默认 O = 2、S = 2(各取 50%)。除数越小,目标可越长;最后取两项限制的较小值。支持小数。"
149
145
  },
150
146
  "Enter a finite divisor of at least 1; decimals are allowed.": {
151
147
  "zh-Hant": "請輸入大於或等於 1 的有限除數,可使用小數。",
@@ -163,9 +159,9 @@ window.__ModuleLoader__.load({
163
159
  "zh-Hant": "儲存偏好後,這些控制項會套用至後續摘要請求;其他模型與壓縮設定保持不變。",
164
160
  "zh-Hans": "保存偏好后,这些控制项将应用于后续摘要请求;其他模型与压缩设置保持不变。"
165
161
  },
166
- "Context uses the smaller of the verified Strata window and the existing DSH model window. The preview ceiling assumes the proposed budgets; existing conversation settings can make the actual target lower.": {
167
- "zh-Hant": "上下文採用已確認的 Strata 視窗與 DSH 既有模型視窗的較小值。預覽上限依提議預算計算,既有對話設定可能讓實際目標更低。",
168
- "zh-Hans": "上下文采用已确认的 Strata 窗口与 DSH 已有模型窗口的较小值。预览上限按提议预算计算,已有对话设置可能让实际目标更低。"
162
+ "The preview uses the proposed summary cap. Actual targets also account for the estimated remaining context and source length; they may be lower.": {
163
+ "zh-Hant": "預覽採用提議的摘要上限。實際目標還會考慮估算的上下文剩餘空間與原文長度,可能更低。",
164
+ "zh-Hans": "预览采用提议的摘要上限。实际目标还会考虑估算的上下文剩余空间与原文长度,可能更低。"
169
165
  },
170
166
  "The original prompt, output cap and other settings stay unchanged. This is a writing target, not a guaranteed length or an optimal value.": {
171
167
  "zh-Hant": "保留原生提示詞、輸出硬上限與其他設定。這是寫作目標,不保證實際長度,也不代表最佳值。",
@@ -431,20 +427,20 @@ REASONING_DEFAULT_CONFLICT: 'The provider default reasoning level is not support
431
427
  };
432
428
  const diagnostic = (message) => translations[message] ? t(message) : language === 'en' ? message : t('Server diagnostic: ') + message;
433
429
  const [state,setState] = React.useState(null), [preview,setPreview] = React.useState(null), [result,setResult] = React.useState(null);
434
- const [form,setForm] = React.useState({providerId:'',output:'',manual:false,auto:false,follow:true,summaryGuidance:true,summaryDivisor:'16',summaryOutputDivisor:'2',summarySourceDivisor:'2',summaryPrompt:''});
430
+ const [form,setForm] = React.useState({providerId:'',output:'',manual:false,auto:false,follow:true,summaryGuidance:false,summaryOutputDivisor:'2',summarySourceDivisor:'2',summaryPrompt:''});
435
431
  const [pending,setPending] = React.useState(false), [error,setError] = React.useState(''), [loadError,setLoadError] = React.useState('');
436
432
  const [notice,setNotice] = React.useState(''), [conflict,setConflict] = React.useState(false);
437
433
  const formRef=React.useRef(form), baseline=React.useRef(null), requestSeq=React.useRef(0), alive=React.useRef(true);
438
434
  const numberValue = value => value.trim()===''?NaN:Number(value);
439
- const values = (f) => ({providerId:f.providerId,outputTokens:f.manual ? (f.output.trim()==='' ? NaN : Number(f.output)) : null,autoSync:f.auto,followLoadedModel:f.follow,summaryGuidance:f.summaryGuidance,summaryContextDivisor:numberValue(f.summaryDivisor),summaryOutputDivisor:numberValue(f.summaryOutputDivisor),summarySourceDivisor:numberValue(f.summarySourceDivisor),summaryPromptTemplate:f.summaryPrompt});
440
- const preferences = (s) => ({providerId:s.providerId || '',outputTokens:s.outputTokens ?? null,autoSync:!!s.autoSync,followLoadedModel:s.followLoadedModel !== false,summaryGuidance:s.summaryGuidance!==false,summaryContextDivisor:s.summaryContextDivisor??16,summaryOutputDivisor:s.summaryOutputDivisor??2,summarySourceDivisor:s.summarySourceDivisor??2,summaryPromptTemplate:s.summaryPromptTemplate??s.defaultSummaryPromptTemplate??''});
435
+ const values = (f) => ({providerId:f.providerId,outputTokens:f.manual ? (f.output.trim()==='' ? NaN : Number(f.output)) : null,autoSync:f.auto,followLoadedModel:f.follow,summaryGuidance:f.summaryGuidance,summaryOutputDivisor:numberValue(f.summaryOutputDivisor),summarySourceDivisor:numberValue(f.summarySourceDivisor),summaryPromptTemplate:f.summaryPrompt});
436
+ const preferences = (s) => ({providerId:s.providerId || '',outputTokens:s.outputTokens ?? null,autoSync:!!s.autoSync,followLoadedModel:s.followLoadedModel !== false,summaryGuidance:s.summaryGuidance===true,summaryOutputDivisor:s.summaryOutputDivisor??2,summarySourceDivisor:s.summarySourceDivisor??2,summaryPromptTemplate:s.summaryPromptTemplate??s.defaultSummaryPromptTemplate??''});
441
437
  const equal = (a,b) => Object.keys(a).every(key=>Object.is(a[key],b[key]));
442
438
  const acceptStatus = React.useCallback((s,force=false) => {
443
439
  if (!alive.current) return;
444
440
  const next=preferences(s), old=baseline.current;
445
441
  const untouched=!old || equal(values(formRef.current),old.values);
446
442
  if (force || untouched) {
447
- const nextForm={providerId:next.providerId,output:next.outputTokens==null?'':String(next.outputTokens),manual:next.outputTokens!=null,auto:next.autoSync,follow:next.followLoadedModel,summaryGuidance:next.summaryGuidance,summaryDivisor:String(next.summaryContextDivisor),summaryOutputDivisor:String(next.summaryOutputDivisor),summarySourceDivisor:String(next.summarySourceDivisor),summaryPrompt:next.summaryPromptTemplate};
443
+ const nextForm={providerId:next.providerId,output:next.outputTokens==null?'':String(next.outputTokens),manual:next.outputTokens!=null,auto:next.autoSync,follow:next.followLoadedModel,summaryGuidance:next.summaryGuidance,summaryOutputDivisor:String(next.summaryOutputDivisor),summarySourceDivisor:String(next.summarySourceDivisor),summaryPrompt:next.summaryPromptTemplate};
448
444
  formRef.current=nextForm; setForm(nextForm); setConflict(false);
449
445
  baseline.current={values:next,revision:s.settingsRevision};
450
446
  } else if (old.revision!==s.settingsRevision || !equal(old.values,next)) {
@@ -477,14 +473,14 @@ REASONING_DEFAULT_CONFLICT: 'The provider default reasoning level is not support
477
473
  } finally { if(alive.current)setPending(false); }
478
474
  }
479
475
  const draft=values(form), isDivisor=value=>Number.isFinite(value)&&value>=1&&value<=Number.MAX_SAFE_INTEGER;
480
- const validDivisor=[draft.summaryContextDivisor,draft.summaryOutputDivisor,draft.summarySourceDivisor].every(isDivisor);
476
+ const validDivisor=[draft.summaryOutputDivisor,draft.summarySourceDivisor].every(isDivisor);
481
477
  const validPrompt=typeof draft.summaryPromptTemplate==='string'&&draft.summaryPromptTemplate.length>0&&draft.summaryPromptTemplate.length<=2048&&draft.summaryPromptTemplate.includes('{target}');
482
478
  const valid=validDivisor&&validPrompt&&(draft.outputTokens===null || Number.isSafeInteger(draft.outputTokens)&&draft.outputTokens>0);
483
479
  const dirty=!!baseline.current&&!equal(draft,baseline.current.values);
484
480
  const displayedResult=!result || state?.lastApplied?.at>result.at ? state?.lastApplied : result;
485
481
  const activeBudget=preview?.budget;
486
482
  const summaryTargetCeiling=form.summaryGuidance&&validDivisor&&Number.isSafeInteger(activeBudget?.contextWindow)&&activeBudget.contextWindow>0&&Number.isSafeInteger(activeBudget?.summaryMaxTokens)&&activeBudget.summaryMaxTokens>0
487
- ? Math.floor(Math.min(activeBudget.contextWindow/draft.summaryContextDivisor,activeBudget.summaryMaxTokens/draft.summaryOutputDivisor)) : null;
483
+ ? Math.floor(activeBudget.summaryMaxTokens/draft.summaryOutputDivisor) : null;
488
484
  const outputSource=(budget)=>t(({server:'Strata Server default',manual:'Manual override',calculated:'Automatic calculation'})[budget?.outputSource]||'Automatic calculation');
489
485
  const preserved=(view)=>{
490
486
  const status=view?.capabilityStatus;
@@ -529,7 +525,9 @@ REASONING_DEFAULT_CONFLICT: 'The provider default reasoning level is not support
529
525
  h('div',{className:'ss-card','data-testid':'summary-target'},
530
526
  h('strong',null,t('Dynamic summary target')),
531
527
  h('label',{className:'ss-check'},h('input',{type:'checkbox','data-testid':'summary-guidance-toggle','aria-label':t('Enable summary guidance'),checked:form.summaryGuidance,onChange:e=>edit({summaryGuidance:e.target.checked}),disabled:pending||!state}),t('Enable summary guidance')),
532
- h('label',null,t('Context divisor (context ÷ N)'),h('input',{type:'number','data-testid':'summary-divisor','aria-label':t('Context divisor (context ÷ N)'),'aria-invalid':!isDivisor(draft.summaryContextDivisor),min:1,max:Number.MAX_SAFE_INTEGER,step:'any',value:form.summaryDivisor,onChange:e=>edit({summaryDivisor:e.target.value}),disabled:pending||!state})),
528
+ h('div',{className:'ss-grid'},
529
+ h('label',null,t('Output budget divisor (estimated budget ÷ O)'),h('input',{type:'number','data-testid':'summary-output-divisor','aria-label':t('Output budget divisor (estimated budget ÷ O)'),'aria-invalid':!isDivisor(draft.summaryOutputDivisor),min:1,max:Number.MAX_SAFE_INTEGER,step:'any',value:form.summaryOutputDivisor,onChange:e=>edit({summaryOutputDivisor:e.target.value}),disabled:pending||!state})),
530
+ h('label',null,t('Source length divisor (source estimate ÷ S)'),h('input',{type:'number','data-testid':'summary-source-divisor','aria-label':t('Source length divisor (source estimate ÷ S)'),'aria-invalid':!isDivisor(draft.summarySourceDivisor),min:1,max:Number.MAX_SAFE_INTEGER,step:'any',value:form.summarySourceDivisor,onChange:e=>edit({summarySourceDivisor:e.target.value}),disabled:pending||!state}))),
533
531
  state&&!validDivisor&&h('p',{className:'ss-status ss-error',role:'alert'},t('Enter a finite divisor of at least 1; decimals are allowed.')),
534
532
  h('label',{className:'ss-prompt-label'},t('Supplemental summary prompt'),h('textarea',{'data-testid':'summary-prompt','aria-label':t('Supplemental summary prompt'),'aria-invalid':!validPrompt,rows:5,value:form.summaryPrompt,onChange:e=>edit({summaryPrompt:e.target.value}),disabled:pending||!state,spellCheck:false})),
535
533
  state&&!validPrompt&&h('p',{className:'ss-status ss-error',role:'alert'},t('Use 1–2,048 characters and include {target}.')),
@@ -540,11 +538,8 @@ REASONING_DEFAULT_CONFLICT: 'The provider default reasoning level is not support
540
538
  ? h('p',{'data-testid':'summary-target-ceiling'},t('Maximum target for this preview: '),format(summaryTargetCeiling),' tokens')
541
539
  : h('p',{className:'ss-status','data-testid':'summary-target-native'},t('This preview is below the 256-token minimum. The native summary prompt remains unchanged.'))),
542
540
  h('details',{className:'ss-details','data-testid':'summary-details'},h('summary',null,t('Calculation and limits')),
543
- h('div',{className:'ss-grid'},
544
- h('label',null,t('Summary output divisor (output limit ÷ O)'),h('input',{type:'number','data-testid':'summary-output-divisor','aria-label':t('Summary output divisor (output limit ÷ O)'),'aria-invalid':!isDivisor(draft.summaryOutputDivisor),min:1,max:Number.MAX_SAFE_INTEGER,step:'any',value:form.summaryOutputDivisor,onChange:e=>edit({summaryOutputDivisor:e.target.value}),disabled:pending||!state})),
545
- h('label',null,t('Source length divisor (source estimate ÷ S)'),h('input',{type:'number','data-testid':'summary-source-divisor','aria-label':t('Source length divisor (source estimate ÷ S)'),'aria-invalid':!isDivisor(draft.summarySourceDivisor),min:1,max:Number.MAX_SAFE_INTEGER,step:'any',value:form.summarySourceDivisor,onChange:e=>edit({summarySourceDivisor:e.target.value}),disabled:pending||!state}))),
546
- h('p',{className:'ss-muted'},t('Target = floor(min(context / N, summary output limit / O, selected content estimate / S)).')),
547
- h('p',{className:'ss-muted'},t('When guidance cannot be applied, DSH keeps its native summary behavior.')))),
541
+ h('p',{className:'ss-muted'},t('Target = floor(min(estimated effective output budget / O, selected content estimate / S)). Default divisors: 2 and 2 (50% each).')),
542
+ h('p',{className:'ss-muted'},t('The preview uses the proposed summary cap. Actual targets also account for the estimated remaining context and source length; they may be lower.'),' ',t('When guidance cannot be applied, DSH keeps its native summary behavior.')))),
548
543
  h('div',{className:'ss-actions'},
549
544
  h('button',{disabled:pending||!state||!form.providerId||!valid||!dirty||conflict,onClick:()=>action(async()=>{
550
545
  const changes=Object.fromEntries(Object.entries(values(formRef.current)).filter(([key,value])=>!Object.is(value,baseline.current.values[key])));
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import { dirname, join } from 'node:path';
4
4
  import z from '@deepseek-ai/schemastery';
5
5
  import { discoverStrata, planBudget, StrataSyncError } from './core.mjs';
6
- import { DEFAULT_SUMMARY_DIVISOR, DEFAULT_SUMMARY_TEMPLATE, validSummaryDivisor, validSummaryTemplate, installSummaryGuidance } from './summary.mjs';
6
+ import { DEFAULT_SUMMARY_TEMPLATE, validSummaryDivisor, validSummaryTemplate, installSummaryGuidance } from './summary.mjs';
7
7
 
8
8
  export const inject = ['configEditor', 'connection', 'agents', 'credentials', 'agentPresets'];
9
9
  export const Config = z.object({
@@ -11,8 +11,7 @@ export const Config = z.object({
11
11
  autoSync: z.boolean().default(false),
12
12
  followLoadedModel: z.boolean().default(true),
13
13
  outputTokens: z.number().step(1).min(1),
14
- summaryGuidance: z.boolean().default(true),
15
- summaryContextDivisor: z.number().min(1).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_SUMMARY_DIVISOR),
14
+ summaryGuidance: z.boolean().default(false),
16
15
  summaryOutputDivisor: z.number().min(1).max(Number.MAX_SAFE_INTEGER).default(2),
17
16
  summarySourceDivisor: z.number().min(1).max(Number.MAX_SAFE_INTEGER).default(2),
18
17
  summaryPromptTemplate: z.string().default(DEFAULT_SUMMARY_TEMPLATE),
@@ -145,7 +144,7 @@ export function apply(ctx, initialConfig) {
145
144
  function publicSettings(value = settings) {
146
145
  return { providerId: value.providerId ?? '', autoSync: value.autoSync ?? false,
147
146
  followLoadedModel: value.followLoadedModel ?? true, outputTokens: value.outputTokens ?? null,
148
- summaryGuidance: value.summaryGuidance ?? true, summaryContextDivisor: value.summaryContextDivisor ?? DEFAULT_SUMMARY_DIVISOR,
147
+ summaryGuidance: value.summaryGuidance ?? false,
149
148
  summaryOutputDivisor: value.summaryOutputDivisor ?? 2, summarySourceDivisor: value.summarySourceDivisor ?? 2,
150
149
  summaryPromptTemplate: value.summaryPromptTemplate ?? DEFAULT_SUMMARY_TEMPLATE };
151
150
  }
@@ -389,7 +388,7 @@ export function apply(ctx, initialConfig) {
389
388
  }
390
389
  async function configure(request) {
391
390
  ensureOpen();
392
- payload(request, ['providerId', 'autoSync', 'followLoadedModel', 'outputTokens', 'summaryGuidance', 'summaryContextDivisor', 'summaryOutputDivisor', 'summarySourceDivisor', 'summaryPromptTemplate', 'expectedRevision']);
391
+ payload(request, ['providerId', 'autoSync', 'followLoadedModel', 'outputTokens', 'summaryGuidance', 'summaryOutputDivisor', 'summarySourceDivisor', 'summaryPromptTemplate', 'expectedRevision']);
393
392
  refreshSettings();
394
393
  if (typeof request.expectedRevision !== 'string' || request.expectedRevision !== hash(publicSettings())) {
395
394
  refuse('stale-settings', 'Sync preferences changed. Reload the latest preferences before saving.');
@@ -405,7 +404,7 @@ export function apply(ctx, initialConfig) {
405
404
  if (typeof request[key] !== 'boolean') refuse('invalid-request', `${key} must be a boolean.`);
406
405
  next[key] = request[key];
407
406
  }
408
- for (const key of ['summaryContextDivisor', 'summaryOutputDivisor', 'summarySourceDivisor']) {
407
+ for (const key of ['summaryOutputDivisor', 'summarySourceDivisor']) {
409
408
  if (!own(request, key)) continue;
410
409
  if (!validSummaryDivisor(request[key])) refuse('invalid-budget', 'Summary divisors must be finite numbers from 1 to Number.MAX_SAFE_INTEGER.');
411
410
  next[key] = request[key];
package/dist/summary.mjs CHANGED
@@ -12,11 +12,9 @@ const augmentedMessages = globalThis[Symbol.for('dsh-strata-sync/summary-augment
12
12
  // Eight mandatory headings need enough space for a meaningful checkpoint. Do
13
13
  // not round a tiny target UP; leave native behavior intact instead.
14
14
  export const MIN_SUMMARY_TARGET = 256;
15
- export const DEFAULT_SUMMARY_DIVISOR = 16;
16
-
17
- export function planSummaryTarget(contextWindow, summaryMaxTokens, sourceTokens, divisor = DEFAULT_SUMMARY_DIVISOR, outputDivisor = 2, sourceDivisor = 2) {
18
- if (![contextWindow, summaryMaxTokens, sourceTokens].every(positive) || ![divisor, outputDivisor, sourceDivisor].every(validSummaryDivisor)) return null;
19
- const target = Math.floor(Math.min(contextWindow / divisor, summaryMaxTokens / outputDivisor, sourceTokens / sourceDivisor));
15
+ export function planSummaryTarget(effectiveOutputTokens, sourceTokens, outputDivisor = 2, sourceDivisor = 2) {
16
+ if (![effectiveOutputTokens, sourceTokens].every(positive) || ![outputDivisor, sourceDivisor].every(validSummaryDivisor)) return null;
17
+ const target = Math.floor(Math.min(effectiveOutputTokens / outputDivisor, sourceTokens / sourceDivisor));
20
18
  return target >= MIN_SUMMARY_TARGET ? target : null;
21
19
  }
22
20
 
@@ -68,6 +66,47 @@ export function estimateSummarySource(messages) {
68
66
  return positive(tokens) ? tokens : null;
69
67
  }
70
68
 
69
+ /** Conservative text-only estimate of the output room before provider dispatch.
70
+ * The supported DSH adapter uses pi-ai 1.1.0: 3.5 chars/token and
71
+ * reserves 4096 tokens before clamping maxTokens. Replayed assistant usage is
72
+ * zeroed by DSH. Include system text, tool schemas and framing as well, even
73
+ * where that adapter's heuristic omits them. This is NOT a tokenizer count or
74
+ * the server's final cap; unknown content leaves the native prompt untouched.
75
+ */
76
+ export function estimateEffectiveSummaryBudget(options, contextWindow, template = DEFAULT_SUMMARY_TEMPLATE) {
77
+ if (!positive(contextWindow) || !positive(options?.maxTokens) || !validSummaryTemplate(template) || !Array.isArray(options.messages)) return null;
78
+ const price = text => Math.ceil(text.length / 3.5) + 4;
79
+ let input = 0;
80
+ try {
81
+ if (options.system !== undefined) {
82
+ if (typeof options.system !== 'string') return null;
83
+ input += price(options.system);
84
+ }
85
+ if (options.tools !== undefined) {
86
+ if (!Array.isArray(options.tools)) return null;
87
+ input += price(JSON.stringify(options.tools));
88
+ }
89
+ for (const message of options.messages) {
90
+ if (!['system', 'user', 'assistant', 'tool'].includes(message?.role) || !Array.isArray(message.content)) return null;
91
+ input += 4;
92
+ if (message.role === 'tool' && !message.content.some(block => block.type === 'text' && block.text?.length)) input += price('(no output)');
93
+ for (const block of message.content) {
94
+ if (['text', 'reasoning'].includes(block?.type) && typeof block.text === 'string') input += price(block.text);
95
+ else if (block?.type === 'tool-call' && typeof block.name === 'string' && typeof block.arguments === 'string') {
96
+ // JSON normalization can expand compact numeric literals (e.g. 1e9).
97
+ const normalized = JSON.stringify(JSON.parse(block.arguments));
98
+ input += price(block.name) + Math.max(price(block.arguments), price(normalized));
99
+ } else return null;
100
+ }
101
+ }
102
+ // Reserve the longest possible rendered supplement once, avoiding a
103
+ // self-referential target calculation. The target never exceeds maxTokens.
104
+ input += price('\n- ' + summaryInstruction(options.maxTokens, template));
105
+ } catch { return null; }
106
+ if (!Number.isSafeInteger(input)) return null;
107
+ return Math.max(0, Math.min(options.maxTokens, contextWindow - input - 4096));
108
+ }
109
+
71
110
  /** The native waterfall's next() closes over options, so replace only its
72
111
  * messages property for the lifetime of this stream; never mutate frozen
73
112
  * messages or recursively dispatch a second LLM request.
@@ -77,13 +116,12 @@ export function installSummaryGuidance(ctx, resolveContext, getSettings = () =>
77
116
  function preferences() {
78
117
  try {
79
118
  const value = getSettings();
80
- const enabled = value.summaryGuidance === undefined ? true : value.summaryGuidance;
81
- const divisor = value.summaryContextDivisor === undefined ? DEFAULT_SUMMARY_DIVISOR : value.summaryContextDivisor;
119
+ const enabled = value.summaryGuidance === undefined ? false : value.summaryGuidance;
82
120
  const outputDivisor = value.summaryOutputDivisor === undefined ? 2 : value.summaryOutputDivisor;
83
121
  const sourceDivisor = value.summarySourceDivisor === undefined ? 2 : value.summarySourceDivisor;
84
122
  const template = value.summaryPromptTemplate === undefined ? DEFAULT_SUMMARY_TEMPLATE : value.summaryPromptTemplate;
85
- return enabled === true && [divisor, outputDivisor, sourceDivisor].every(validSummaryDivisor) && validSummaryTemplate(template)
86
- ? { divisor, outputDivisor, sourceDivisor, template } : null;
123
+ return enabled === true && [outputDivisor, sourceDivisor].every(validSummaryDivisor) && validSummaryTemplate(template)
124
+ ? { outputDivisor, sourceDivisor, template } : null;
87
125
  } catch { return null; }
88
126
  }
89
127
  const remove = ctx.on('llm/stream', (options, next) => {
@@ -103,7 +141,8 @@ export function installSummaryGuidance(ctx, resolveContext, getSettings = () =>
103
141
  let contextWindow;
104
142
  try { contextWindow = await resolveContext(options); }
105
143
  catch { /* Metadata/compatibility failure must not break native inference. */ }
106
- const target = planSummaryTarget(contextWindow, options.maxTokens, sourceTokens, selected.divisor, selected.outputDivisor, selected.sourceDivisor);
144
+ const effectiveOutput = estimateEffectiveSummaryBudget(options, contextWindow, selected.template);
145
+ const target = planSummaryTarget(effectiveOutput, sourceTokens, selected.outputDivisor, selected.sourceDivisor);
107
146
  const current = preferences();
108
147
  if (!closed && !options.signal?.aborted && current && Object.keys(selected).every(key => selected[key] === current[key]) && target !== null && options.messages === original) {
109
148
  const last = { ...native, content: [{ ...native.content[0], text: `${native.content[0].text}\n- ${summaryInstruction(target, selected.template)}` }] };
@@ -69,7 +69,7 @@ dsh plugin --profile desktop add dsh-strata-sync --registry=https://registry.npm
69
69
 
70
70
  ## 摘要提示词与目标长度
71
71
 
72
- **动态摘要目标**默认开启,在 DSH 原生摘要指令后补充要求;关闭后仍保留原生摘要与压缩行为。
72
+ **动态摘要目标**默认关闭。启用后会在 DSH 原生摘要指令后补充要求;关闭时使用 DSH 原生摘要与压缩行为。
73
73
 
74
74
  可编辑的**摘要补充提示词**默认内容为:
75
75
 
@@ -79,17 +79,20 @@ Keep the entire checkpoint, including all required headings, within a target of
79
79
 
80
80
  请保留 `{target}`,每次支持的摘要请求前,它会替换成计算出的 token 目标。模板最多 2,048 个字符。点击**恢复默认提示词**可重置内容,再点击**保存偏好**应用修改。
81
81
 
82
- 展开**计算方式与限制**,可以调整其余比例:
82
+ 两个除数控制目标,默认均为 **2(各取 50%)**:
83
83
 
84
84
  | 除数 | 默认值 | 将目标限制为 |
85
85
  |---|---:|---|
86
- | 上下文(N) | 16 | 上下文 ÷ N |
87
- | 摘要输出(O) | 2 | 本次摘要实际输出上限 ÷ O |
86
+ | 摘要输出(O) | 2 | 估算有效摘要输出预算 ÷ O |
88
87
  | 原文长度(S) | 2 | 待摘要内容的估算长度 ÷ S |
89
88
 
90
- 目标取三项结果的最小值,再向下取整。所有除数均支持大于或等于 1 的小数。增加除数会降低对应上限,但最终目标仍可能由另一项上限决定。上下文取已确认的服务器与客户端窗口中较小者,原文估算不包含系统提示词和工具定义。
89
+ 目标取两项结果的较小值,再向下取整。除数支持大于或等于 1 的小数。已移除上下文除数;旧设置中的 `summaryContextDivisor` 不再参与计算。已有输出/原文除数及明确的启用选择会保留,新安装或未指定时仍**默认关闭**。
91
90
 
92
- 32K 上下文、摘要上限为 4,096 tokens 且原文足够长时,默认目标为 **2,048 tokens**。这是写作指令,**不是输出硬上限,也不能保证不截断**。界面预览的是目标上限,实际请求的目标可能更小。
91
+ 有效输出预算是**估算值**,不是直接把请求的 `maxTokens` 当作服务器确认的上限。它取本次摘要输出上限与估算上下文剩余空间中的较小值;后者使用已确认的服务器/客户端窗口较小值,扣除完整提示内容(包含系统提示、工具定义和补充句),再预留支持的 pi-ai 适配器所用的 4,096 tokens 安全空间。文本以每 token 约 3.5 字符并加上格式成本估算。原文长度另采用 DSH 的每 token 约 4 字符估算,不含系统提示、工具和摘要指令。两者都不是精确 tokenizer 计数。
92
+
93
+ 例如,有效输出预算为 4,096 tokens 且原文足够长时,32K 与 64K 上下文的目标都是 **2,048 tokens**;原文只有约 2,000 tokens 时,目标为 **1,000 tokens**。上下文剩余空间不足时,目标会进一步降低。界面仅以提议的摘要上限预览目标上界,尚未扣除个别请求的成本。
94
+
95
+ 这是写作指令,**不是输出硬上限,也不能保证不截断**。思考可能共用输出预算;插件不会修改思考设置、`maxTokens`、`fit_max_tokens` 或压缩时机。服务器实际 tokenizer、后续中间层或不同适配器版本仍可能产生更低的上限。
93
96
 
94
97
  保存摘要偏好不会改变模型预算,也不会替换原生提示词或增加额外推理、重试循环。无法验证请求、无法安全估算内容,或目标小于 256 tokens 时,会沿用原本的摘要请求。
95
98
 
@@ -69,7 +69,7 @@ dsh plugin --profile desktop add dsh-strata-sync --registry=https://registry.npm
69
69
 
70
70
  ## 摘要提示詞與目標長度
71
71
 
72
- **動態摘要目標**預設開啟,在 DSH 原生摘要指令後補充要求;關閉後仍保留原生摘要與壓縮行為。
72
+ **動態摘要目標**預設關閉。啟用後會在 DSH 原生摘要指令後補充要求;關閉時使用 DSH 原生摘要與壓縮行為。
73
73
 
74
74
  可編輯的**摘要補充提示詞**預設內容為:
75
75
 
@@ -79,17 +79,20 @@ Keep the entire checkpoint, including all required headings, within a target of
79
79
 
80
80
  請保留 `{target}`,每次支援的摘要請求前,它會替換成計算出的 token 目標。範本最多 2,048 個字元。點選**還原預設提示詞**可重設內容,再點選**儲存偏好**套用修改。
81
81
 
82
- 展開**計算方式與限制**,可以調整其餘比例:
82
+ 兩個除數控制目標,預設均為 **2(各取 50%)**:
83
83
 
84
84
  | 除數 | 預設值 | 將目標限制為 |
85
85
  |---|---:|---|
86
- | 上下文(N) | 16 | 上下文 ÷ N |
87
- | 摘要輸出(O) | 2 | 本次摘要實際輸出上限 ÷ O |
86
+ | 摘要輸出(O) | 2 | 估算有效摘要輸出預算 ÷ O |
88
87
  | 原文長度(S) | 2 | 待摘要內容的估算長度 ÷ S |
89
88
 
90
- 目標取三項結果的最小值,再向下取整。所有除數均支援大於或等於 1 的小數。增加除數會降低對應上限,但最終目標仍可能由另一項上限決定。上下文取已確認的伺服器與客戶端視窗中較小者,原文估算不包含系統提示詞和工具定義。
89
+ 目標取兩項結果的較小值,再向下取整。除數支援大於或等於 1 的小數。已移除上下文除數;舊設定中的 `summaryContextDivisor` 不再參與計算。既有輸出/原文除數及明確的啟用選擇會保留,新安裝或未指定時仍**預設關閉**。
91
90
 
92
- 32K 上下文、摘要上限為 4,096 tokens 且原文足夠長時,預設目標為 **2,048 tokens**。這是寫作指令,**不是輸出硬上限,也不能保證不截斷**。介面預覽的是目標上限,實際請求的目標可能更小。
91
+ 有效輸出預算是**估算值**,不是直接把請求的 `maxTokens` 當作伺服器確認的上限。它取本次摘要輸出上限與估算上下文剩餘空間中的較小值;後者使用已確認的伺服器/客戶端視窗較小值,扣除完整提示內容(包含系統提示、工具定義和補充句),再預留支援的 pi-ai 轉接器所用的 4,096 tokens 安全空間。文字以每 token 約 3.5 字元並加上格式成本估算。原文長度另採 DSH 的每 token 約 4 字元估算,不含系統提示、工具和摘要指令。兩者都不是精確 tokenizer 計數。
92
+
93
+ 例如,有效輸出預算為 4,096 tokens 且原文足夠長時,32K 與 64K 上下文的目標都是 **2,048 tokens**;原文只有約 2,000 tokens 時,目標為 **1,000 tokens**。上下文剩餘空間不足時,目標會進一步降低。介面僅以提議的摘要上限預覽目標上界,尚未扣除個別請求的成本。
94
+
95
+ 這是寫作指令,**不是輸出硬上限,也不能保證不截斷**。思考可能共用輸出預算;外掛不會修改思考設定、`maxTokens`、`fit_max_tokens` 或壓縮時機。伺服器實際 tokenizer、後續中介層或不同轉接器版本仍可能產生更低的上限。
93
96
 
94
97
  儲存摘要偏好不會改變模型預算,也不會替換原生提示詞或增加額外推論、重試循環。無法驗證請求、無法安全估算內容,或目標小於 256 tokens 時,會沿用原本的摘要請求。
95
98
 
@@ -2,6 +2,20 @@
2
2
 
3
3
  Validation date: 2026-10-30 (Asia/Taipei).
4
4
 
5
+ ## Two-bound summary target 0.10.4
6
+
7
+ - The target uses only estimated effective output budget / output divisor and selected-source estimate / source divisor. Both divisors default to 2 (50%). The context-divisor control and formula term are removed; legacy stored context divisors are ignored without enabling guidance.
8
+ - Guidance remains off by default in schema, host fallback, request middleware and all three UI locales. Explicit enable choices, output/source divisors and editable templates remain supported. No production settings were edited. Installing the release is left to the user.
9
+ - Effective output room is estimated conservatively against the supported pi-ai 1.1.0 context clamp, including the supplement, system text and tools. It is not a server tokenizer result. Reasoning and later request transformations can still reduce usable answer space.
10
+ - All **155 automated tests pass**. Tests cover the actual pi-ai clamp on multiple context/input sizes, near-full and exhausted context, independent source length, additional prompt costs, ignored legacy divisors, disabled-mode zero discovery, restored native requests and localized controls. The package verifier also passes.
11
+ - No model inference requests were sent. Tests do not prove truncation is impossible; this supplement is a writing target and leaves native hard caps, reasoning and server configuration unchanged.
12
+
13
+ ## Earlier opt-in summary validation (before the two-bound update)
14
+
15
+ - Summary guidance now defaults to `false` in the schema, host fallback, request hook and UI. An existing explicit `true` remains enabled; all other budgets and summary parameters are preserved.
16
+ - Added a three-language UI regression for an omitted preference. Host and middleware tests verify that omitted/disabled guidance preserves native requests and makes no metadata calls; explicit opt-in still appends the configured sentence.
17
+ - All 152 automated tests pass, including 39 UI tests and the package verifier. No inference requests were sent.
18
+
5
19
  ## Concise summary help 0.10.1
6
20
 
7
21
  - Reduces the expanded summary help to the calculation formula and one native-fallback sentence in all three locales. Full explanations remain in the READMEs. No runtime behavior, settings, defaults or prompt text changes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-strata-sync",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "Native DSH settings UI for synchronizing Strata Server models, context and compaction budgets",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",