dsh-instruction-memory 1.0.16 → 1.0.17
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/CHANGELOG.md +30 -0
- package/README.md +5 -0
- package/lib/client.js +96 -0
- package/lib/index.js +82 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@
|
|
|
7
7
|
> 1.0.13 起由 CI 经 OIDC Trusted Publishing 发布(首个带 provenance 的版本)。
|
|
8
8
|
> 这些历史 tag **不应回填**——npm 上的版本号已被占用。
|
|
9
9
|
|
|
10
|
+
## [1.0.17] — 2026-10-12
|
|
11
|
+
|
|
12
|
+
### 新增
|
|
13
|
+
|
|
14
|
+
- **注入预算可视化(F1)**:设置页在「注入字数上限」下方显示用量条与「实际注入 x / y 字符
|
|
15
|
+
(约 z token,估算)」(token 按 0.6 系数估算,与既有说明同一口径,仅作参考);被上限略过的
|
|
16
|
+
**生效中**条目在列表中逐条标注橙色「本轮不注入」(悬停说明原因),极端情况下首条被迫截断时
|
|
17
|
+
标注红色「已截断」,用量条同时变为警示色并给出条数提示。调高上限或调整优先级保存后标记即时变化。
|
|
18
|
+
- Host 新增导出纯函数 `planInjection(data)`:在**不改变注入文本一个字节**的前提下,返回注入文本、
|
|
19
|
+
生效上限(clamp 后的 budget)、候选条目数,以及每条 `included`(`{id, chars, truncated}`)
|
|
20
|
+
/ `omitted`(`{id, chars}`)的取舍明细与考虑顺序(先「始终」后「按需」、同档优先级/更新时间降序);
|
|
21
|
+
`buildBlock()` 改为它的薄封装。
|
|
22
|
+
- 快照 `injection` 新增 `budget` / `activeCount` / `included` / `omitted` 字段(SSE 帧随之自动携带)。
|
|
23
|
+
存储格式仍为 v1、**无新增落盘字段**;旧客户端收到新字段自然忽略,新客户端对没有这些字段的旧宿主
|
|
24
|
+
快照回退到 `data.budgetChars`、不显示条目标记,两侧均不报错。
|
|
25
|
+
|
|
26
|
+
### 测试
|
|
27
|
+
|
|
28
|
+
- `smoke-test.mjs` 新增 20+ 条 `planInjection` 断言:空/总开关关闭/非法输入、budget 越界夹取、
|
|
29
|
+
与 `buildBlock()` 逐字节一致、停用与空条目不参与、「始终」先于「按需」、紧预算下 included/omitted
|
|
30
|
+
恰好分区全部候选、单条超长首条截断标记、计划本身确定性。
|
|
31
|
+
- `contract-test.mjs` 新增独立验证块:经真实路由在 800 字符上限下断言快照预算、`chars === preview.length`
|
|
32
|
+
且不超上限、取舍明细恰好覆盖全部生效条目且不含停用条目、预览脚注数量与 omitted 一致、提高上限后
|
|
33
|
+
全部纳入;并把真实/旧版/垃圾三类快照喂给客户端 reducer 验证接缝。
|
|
34
|
+
- `verify.mjs` 新增客户端 `__budgetMeter` reducer 断言(计划字段优先、旧快照回退、非法项忽略、
|
|
35
|
+
垃圾输入零值不抛错、percent 封顶 100、0.6 token 估算)及 6 个新 i18n 键的双语存在性;
|
|
36
|
+
键集与占位符一致性仍由既有用例锁定。
|
|
37
|
+
- `docs/demo.html` 的演示宿主改为实时计算注入计划(与 `planInjection` 同一算法),演示数据
|
|
38
|
+
默认 800 字符上限以直接展示用量条与「本轮不注入」标记;旧的手工静态预览文本移除。
|
|
39
|
+
|
|
10
40
|
## [1.0.16] — 2026-10-05
|
|
11
41
|
|
|
12
42
|
一次定向缺陷修复 + 宿主范围扩展。每项修复都有对应回归断言(`npm test` 五套件全绿:
|
package/README.md
CHANGED
|
@@ -54,6 +54,11 @@ dsh plugin --profile web remove dsh-instruction-memory
|
|
|
54
54
|
上限单位是**字符**而非 token——按 DeepSeek 官方分词比例约 1 个中文字符 ≈ 0.6 token、
|
|
55
55
|
1 个英文字符 ≈ 0.3 token(不同模型分词有差异,实际用量以模型返回为准),上限 40000
|
|
56
56
|
字符约合 24000 token。
|
|
57
|
+
- **预算用量可视化**:设置页在上限输入框下方显示用量条——「实际注入 x / y 字符(约 z token,
|
|
58
|
+
估算)」(token 数按 0.6 系数估算,仅作参考);被上限略过的**生效中**条目会在列表里逐条
|
|
59
|
+
标注橙色「本轮不注入」(悬停可见原因),极端情况下首条被迫截断时标注红色「已截断」。
|
|
60
|
+
调高上限或调整优先级后标记实时变化。这些数据只是对注入取舍的**展示**,不改变注入文本本身;
|
|
61
|
+
存储格式不变,旧宿主/旧客户端对新字段一律忽略,互不影响。
|
|
57
62
|
|
|
58
63
|
条目全部停用或总开关关闭时,该提示词段落**整个注销**,系统提示词与未安装本插件时逐字节一致。
|
|
59
64
|
|
package/lib/client.js
CHANGED
|
@@ -58,6 +58,12 @@ window.__ModuleLoader__.load({
|
|
|
58
58
|
.im-tag-always { color:var(--dsw-alias-brand-primary,#3b6cff); border-color:currentColor; }
|
|
59
59
|
.im-tag-auto { color:var(--dsw-alias-state-success-primary,#18a058); border-color:currentColor; }
|
|
60
60
|
.im-tag-high { color:var(--dsw-alias-state-warn-primary,#d97706); border-color:currentColor; }
|
|
61
|
+
.im-tag-off { color:var(--dsw-alias-state-warn-primary,#d97706); border-color:currentColor; }
|
|
62
|
+
.im-tag-cut { color:var(--dsw-alias-state-error-primary,#d94838); border-color:currentColor; }
|
|
63
|
+
.im-meter { height:6px; border-radius:999px; overflow:hidden;
|
|
64
|
+
background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.08)); }
|
|
65
|
+
.im-meter-fill { height:100%; border-radius:999px; background:var(--dsw-alias-brand-primary,#3b6cff); }
|
|
66
|
+
.im-meter-fill-warn { background:var(--dsw-alias-state-warn-primary,#d97706); }
|
|
61
67
|
.im-empty { padding:26px; border-radius:10px; text-align:center; font-size:12px;
|
|
62
68
|
border:1px dashed var(--dsw-alias-border-l2,rgba(0,0,0,.18)); color:var(--dsw-alias-label-secondary,#8a9099); }
|
|
63
69
|
.im-err { font-size:12px; color:var(--dsw-alias-state-error-primary,#d94838); }
|
|
@@ -79,6 +85,7 @@ window.__ModuleLoader__.load({
|
|
|
79
85
|
.im-body-open { max-height:none; }
|
|
80
86
|
.im-more { margin-top:4px; font-size:11.5px; color:var(--dsw-alias-brand-primary,#3b6cff); cursor:pointer; user-select:none; }
|
|
81
87
|
.im-search-meta { margin-top:5px; }
|
|
88
|
+
.im-meter-line { margin-top:5px; }
|
|
82
89
|
.im-undo { display:flex; align-items:center; gap:10px; padding:9px 13px; border-radius:8px; font-size:12px;
|
|
83
90
|
border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.14)); background:var(--dsw-alias-bg-layer-2,rgba(0,0,0,.04)); }
|
|
84
91
|
.im-undo button { border:1px solid var(--dsw-alias-brand-primary,#3b6cff); color:var(--dsw-alias-brand-primary,#3b6cff);
|
|
@@ -140,6 +147,12 @@ window.__ModuleLoader__.load({
|
|
|
140
147
|
'budget.unitHint': '单位是字符,不是 token',
|
|
141
148
|
'budget.rangeError': '上限需在 {min} ~ {max} 之间',
|
|
142
149
|
'budget.note': '单位为字符、非 token:按 DeepSeek 分词约 1 个中文字符 ≈ 0.6 token(英文 ≈ 0.3),{max} 字符上限约合 {tokens} token,实际以模型为准。',
|
|
150
|
+
'budget.used': '实际注入 {used} / {budget} 字符(约 {tokens} token,估算)',
|
|
151
|
+
'budget.omitted': '有 {count} 条生效指令因超出上限本轮未注入,可调高上限或调整优先级',
|
|
152
|
+
'tag.omitted': '本轮不注入',
|
|
153
|
+
'tag.truncated': '已截断',
|
|
154
|
+
'tag.omittedTitle': '因超出注入字数上限,本条当前不会进入系统提示词',
|
|
155
|
+
'tag.truncatedTitle': '本条过长,仅开头部分进入系统提示词',
|
|
143
156
|
'rules.label': '生效规则',
|
|
144
157
|
'rules.note': '「始终」无条件生效;「按需」由模型按相关性自动判断。超出上限时按优先级保留,其余暂不注入。',
|
|
145
158
|
'search.placeholder': '搜索指令:标题 / 内容 / 适用场景(共 {count} 条)',
|
|
@@ -217,6 +230,12 @@ window.__ModuleLoader__.load({
|
|
|
217
230
|
'budget.unitHint': 'The unit is characters, not tokens',
|
|
218
231
|
'budget.rangeError': 'The limit must be between {min} and {max}',
|
|
219
232
|
'budget.note': 'Characters, not tokens: with DeepSeek tokenization roughly 1 Chinese character ≈ 0.6 token (English ≈ 0.3), the {max}-character ceiling is about {tokens} tokens; actual usage depends on the model.',
|
|
233
|
+
'budget.used': '{used} of {budget} characters actually injected (≈{tokens} tokens, estimate)',
|
|
234
|
+
'budget.omitted': '{count} active entries are not injected this turn because the limit is exceeded; raise the limit or adjust priorities',
|
|
235
|
+
'tag.omitted': 'Not injected',
|
|
236
|
+
'tag.truncated': 'Truncated',
|
|
237
|
+
'tag.omittedTitle': 'This entry currently stays out of the system prompt because the injection size limit is exceeded',
|
|
238
|
+
'tag.truncatedTitle': 'This entry is too long; only its beginning reaches the system prompt',
|
|
220
239
|
'rules.label': 'How instructions apply',
|
|
221
240
|
'rules.note': '“Always” applies unconditionally; “On demand” lets the model judge by relevance. When the limit is exceeded, higher-priority entries are kept and the rest are skipped.',
|
|
222
241
|
'search.placeholder': 'Search instructions: title / content / scenario ({count} in total)',
|
|
@@ -497,6 +516,41 @@ window.__ModuleLoader__.load({
|
|
|
497
516
|
return focused ? currentText : String(incoming)
|
|
498
517
|
}
|
|
499
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Reduce a snapshot's injection plan to the values the header meter and
|
|
521
|
+
* the entry badges render. Pinned as a pure export so the fallback math is
|
|
522
|
+
* testable without React.
|
|
523
|
+
*
|
|
524
|
+
* Tolerates an older Host whose snapshot predates the plan fields: the
|
|
525
|
+
* ceiling then falls back to data.budgetChars and there are simply no
|
|
526
|
+
* per-entry marks (included/omitted). Garbage input degrades to zeros
|
|
527
|
+
* instead of throwing mid-render.
|
|
528
|
+
*/
|
|
529
|
+
function budgetMeter(snapshot) {
|
|
530
|
+
const inj = snapshot !== null && typeof snapshot === 'object'
|
|
531
|
+
&& snapshot.injection !== null && typeof snapshot.injection === 'object'
|
|
532
|
+
? snapshot.injection : {}
|
|
533
|
+
const data = snapshot !== null && typeof snapshot === 'object'
|
|
534
|
+
&& snapshot.data !== null && typeof snapshot.data === 'object'
|
|
535
|
+
? snapshot.data : {}
|
|
536
|
+
const budget = typeof inj.budget === 'number' && inj.budget > 0
|
|
537
|
+
? inj.budget
|
|
538
|
+
: (typeof data.budgetChars === 'number' && data.budgetChars > 0 ? data.budgetChars : 0)
|
|
539
|
+
const used = typeof inj.chars === 'number' && inj.chars >= 0 ? inj.chars : 0
|
|
540
|
+
const countFlagged = (list, wantTruncated) =>
|
|
541
|
+
Array.isArray(list)
|
|
542
|
+
? list.filter((item) => item !== null && typeof item === 'object'
|
|
543
|
+
&& typeof item.id === 'string' && item.id !== ''
|
|
544
|
+
&& (wantTruncated ? item.truncated === true : true)).length
|
|
545
|
+
: 0
|
|
546
|
+
const omitted = countFlagged(inj.omitted, false)
|
|
547
|
+
const truncated = countFlagged(inj.included, true)
|
|
548
|
+
const percent = budget > 0 ? Math.min(100, Math.round((used / budget) * 100)) : 0
|
|
549
|
+
// The same rough ratio the budget note quotes; displayed as an ESTIMATE.
|
|
550
|
+
const tokens = Math.round(used * 0.6)
|
|
551
|
+
return { budget, used, omitted, truncated, percent, tokens }
|
|
552
|
+
}
|
|
553
|
+
|
|
500
554
|
async function callHost(method, args) {
|
|
501
555
|
// A hung host used to pin the panel on 保存中… forever; fail visibly
|
|
502
556
|
// after a generous timeout instead.
|
|
@@ -837,6 +891,23 @@ window.__ModuleLoader__.load({
|
|
|
837
891
|
const injection = view.injection || { registered: false, chars: 0, error: null }
|
|
838
892
|
const entries = data.entries.slice().sort((a, b) => (b.priority - a.priority) || (b.updatedAt - a.updatedAt))
|
|
839
893
|
const activeCount = entries.filter((e) => e.enabled && e.content !== '').length
|
|
894
|
+
// Budget usage + per-entry marks straight from the Host's injection plan.
|
|
895
|
+
// Older hosts simply publish none of these fields: the meter falls back
|
|
896
|
+
// to data.budgetChars and the maps stay empty.
|
|
897
|
+
const meter = budgetMeter(view)
|
|
898
|
+
const omittedById = new Map()
|
|
899
|
+
const truncatedById = new Map()
|
|
900
|
+
{
|
|
901
|
+
const planItems = injection && typeof injection === 'object' ? injection : {}
|
|
902
|
+
for (const item of Array.isArray(planItems.omitted) ? planItems.omitted : []) {
|
|
903
|
+
if (item !== null && typeof item === 'object' && typeof item.id === 'string') omittedById.set(item.id, item)
|
|
904
|
+
}
|
|
905
|
+
for (const item of Array.isArray(planItems.included) ? planItems.included : []) {
|
|
906
|
+
if (item !== null && typeof item === 'object' && item.truncated === true && typeof item.id === 'string') {
|
|
907
|
+
truncatedById.set(item.id, item)
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
840
911
|
const keyword = query.trim().toLowerCase()
|
|
841
912
|
const visible = keyword === ''
|
|
842
913
|
? entries
|
|
@@ -902,6 +973,19 @@ window.__ModuleLoader__.load({
|
|
|
902
973
|
h('label', { className: 'im-label' }, t('rules.label')),
|
|
903
974
|
h('div', { className: 'im-meta' },
|
|
904
975
|
t('rules.note')))),
|
|
976
|
+
(data.enabled === true && activeCount > 0)
|
|
977
|
+
? h('div', { key: 'meter' },
|
|
978
|
+
h('div', { className: 'im-meter' },
|
|
979
|
+
h('div', {
|
|
980
|
+
className: 'im-meter-fill' + (meter.omitted > 0 ? ' im-meter-fill-warn' : ''),
|
|
981
|
+
style: { width: meter.percent + '%' },
|
|
982
|
+
})),
|
|
983
|
+
h('div', { className: 'im-meta im-meter-line' },
|
|
984
|
+
t('budget.used', { used: meter.used, budget: meter.budget, tokens: meter.tokens })),
|
|
985
|
+
meter.omitted > 0
|
|
986
|
+
? h('div', { className: 'im-warn im-meter-line' }, t('budget.omitted', { count: meter.omitted }))
|
|
987
|
+
: null)
|
|
988
|
+
: null,
|
|
905
989
|
entries.length > 0 ? h('div', { key: 'search' },
|
|
906
990
|
h('input', {
|
|
907
991
|
className: 'im-input',
|
|
@@ -987,6 +1071,17 @@ window.__ModuleLoader__.load({
|
|
|
987
1071
|
]
|
|
988
1072
|
if (entry.priority === 2) tags.push(h('span', { className: 'im-tag im-tag-high', key: 'p' }, t('priority.high')))
|
|
989
1073
|
else if (entry.priority === 0) tags.push(h('span', { className: 'im-tag', key: 'p' }, t('priority.low')))
|
|
1074
|
+
// Budget marks from the Host's injection plan. Only active entries can
|
|
1075
|
+
// appear in the plans, so a disabled entry never gets a stale mark.
|
|
1076
|
+
if (omittedById.has(entry.id)) {
|
|
1077
|
+
tags.push(h('span', {
|
|
1078
|
+
className: 'im-tag im-tag-off', key: 'off', title: t('tag.omittedTitle'),
|
|
1079
|
+
}, t('tag.omitted')))
|
|
1080
|
+
} else if (truncatedById.has(entry.id)) {
|
|
1081
|
+
tags.push(h('span', {
|
|
1082
|
+
className: 'im-tag im-tag-cut', key: 'cut', title: t('tag.truncatedTitle'),
|
|
1083
|
+
}, t('tag.truncated')))
|
|
1084
|
+
}
|
|
990
1085
|
return h('div', {
|
|
991
1086
|
className: 'im-item' + (entry.enabled ? '' : ' im-item-off'),
|
|
992
1087
|
key: entry.id,
|
|
@@ -1136,6 +1231,7 @@ window.__ModuleLoader__.load({
|
|
|
1136
1231
|
exports.__confirmDiscard = confirmDiscard
|
|
1137
1232
|
exports.__importSizeVerdict = importSizeVerdict
|
|
1138
1233
|
exports.__nextBudgetText = nextBudgetText
|
|
1234
|
+
exports.__budgetMeter = budgetMeter
|
|
1139
1235
|
exports.__strings = STRINGS
|
|
1140
1236
|
exports.__ns = NS
|
|
1141
1237
|
exports.__dictionaryT = dictionaryT
|
package/lib/index.js
CHANGED
|
@@ -518,19 +518,41 @@ function formatEntry(entry, tag) {
|
|
|
518
518
|
}
|
|
519
519
|
|
|
520
520
|
/**
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
521
|
+
* Build the injection plan: the exact text assembled into the system prompt
|
|
522
|
+
* PLUS the per-entry decisions behind it, so the settings page can show how
|
|
523
|
+
* much of the character budget is in use and flag the enabled entries the
|
|
524
|
+
* current budget leaves out.
|
|
525
|
+
*
|
|
526
|
+
* Deliberately deterministic: identical memory produces a byte-identical
|
|
527
|
+
* plan on every step, so the prompt prefix stays cacheable across a session.
|
|
528
|
+
*
|
|
529
|
+
* Returned shape:
|
|
530
|
+
* {
|
|
531
|
+
* text: string — exactly what buildBlock() returns
|
|
532
|
+
* budget: number — the clamped budgetChars in effect
|
|
533
|
+
* activeCount: number — enabled, non-blank candidates considered
|
|
534
|
+
* included: [{ id, chars, truncated }] — entries that reach the prompt,
|
|
535
|
+
* in consideration order (始终 first, then 按需; within
|
|
536
|
+
* each, priority desc then updatedAt desc); chars is the
|
|
537
|
+
* rendered length of that entry's block (its cut length
|
|
538
|
+
* when truncated)
|
|
539
|
+
* omitted: [{ id, chars }] — active entries the budget could not fit;
|
|
540
|
+
* chars is the full rendered block length they would need
|
|
541
|
+
* }
|
|
542
|
+
*
|
|
543
|
+
* Disabled or blank entries are candidates for neither list: they are not
|
|
544
|
+
* competing for the budget at all. When nothing renders, `text` is '' and
|
|
545
|
+
* `included` is [] even though `budget` still reports the effective ceiling.
|
|
524
546
|
*/
|
|
525
|
-
export function
|
|
526
|
-
|
|
547
|
+
export function planInjection(data) {
|
|
548
|
+
const budget = clampBudget(data !== null && typeof data === 'object' ? data.budgetChars : undefined)
|
|
549
|
+
const blank = () => ({ text: '', budget, activeCount: 0, included: [], omitted: [] })
|
|
550
|
+
if (data === null || typeof data !== 'object' || data.enabled !== true) return blank()
|
|
527
551
|
const active = (Array.isArray(data.entries) ? data.entries : [])
|
|
528
552
|
.filter((entry) => entry.enabled === true && entry.content !== '')
|
|
529
|
-
if (active.length === 0) return
|
|
553
|
+
if (active.length === 0) return blank()
|
|
530
554
|
active.sort((a, b) => (b.priority - a.priority) || (b.updatedAt - a.updatedAt))
|
|
531
555
|
|
|
532
|
-
const budget = clampBudget(data.budgetChars)
|
|
533
|
-
|
|
534
556
|
// Richest preamble that still leaves room for one real entry plus the
|
|
535
557
|
// worst-case omission footer. A tight budget degrades the preamble instead of
|
|
536
558
|
// silently injecting nothing.
|
|
@@ -565,6 +587,8 @@ export function buildBlock(data) {
|
|
|
565
587
|
}
|
|
566
588
|
|
|
567
589
|
const blocks = []
|
|
590
|
+
const included = []
|
|
591
|
+
const omittedPlan = []
|
|
568
592
|
let used = header.length
|
|
569
593
|
let omitted = 0
|
|
570
594
|
|
|
@@ -576,6 +600,7 @@ export function buildBlock(data) {
|
|
|
576
600
|
if (used + block.length + 2 + reserve <= budget) {
|
|
577
601
|
used += block.length + 2
|
|
578
602
|
blocks.push(block)
|
|
603
|
+
included.push({ id: entry.id, chars: block.length, truncated: false })
|
|
579
604
|
return
|
|
580
605
|
}
|
|
581
606
|
// The first entry always gets in, truncated if the budget demands it: a
|
|
@@ -593,17 +618,36 @@ export function buildBlock(data) {
|
|
|
593
618
|
const body = head + note
|
|
594
619
|
blocks.push(body)
|
|
595
620
|
used += body.length + 2
|
|
621
|
+
included.push({ id: entry.id, chars: body.length, truncated: true })
|
|
596
622
|
return
|
|
597
623
|
}
|
|
598
624
|
}
|
|
599
625
|
omitted += 1
|
|
626
|
+
omittedPlan.push({ id: entry.id, chars: block.length })
|
|
600
627
|
}
|
|
601
628
|
|
|
602
629
|
for (const entry of active) if (entry.mode === 'always') consider(entry, '始终')
|
|
603
630
|
for (const entry of active) if (entry.mode !== 'always') consider(entry, '按需')
|
|
604
631
|
|
|
605
|
-
if (blocks.length === 0)
|
|
606
|
-
|
|
632
|
+
if (blocks.length === 0) {
|
|
633
|
+
return { text: '', budget, activeCount: active.length, included: [], omitted: omittedPlan }
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
text: header + blocks.join('\n\n') + footerFor(omitted),
|
|
637
|
+
budget,
|
|
638
|
+
activeCount: active.length,
|
|
639
|
+
included,
|
|
640
|
+
omitted: omittedPlan,
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Render the injected block text. A thin wrapper over planInjection: the
|
|
646
|
+
* decision process lives in one place, and callers that only need the text
|
|
647
|
+
* (the prompt section) keep this byte-stable entry point.
|
|
648
|
+
*/
|
|
649
|
+
export function buildBlock(data) {
|
|
650
|
+
return planInjection(data).text
|
|
607
651
|
}
|
|
608
652
|
|
|
609
653
|
/* ------------------------------------------------------------------ *
|
|
@@ -725,6 +769,22 @@ export function apply(ctx) {
|
|
|
725
769
|
}
|
|
726
770
|
}
|
|
727
771
|
|
|
772
|
+
/**
|
|
773
|
+
* The full injection plan (text + per-entry budget decisions) behind the
|
|
774
|
+
* registered section. The snapshot publishes it so the settings page can
|
|
775
|
+
* show budget usage and mark the enabled entries that currently do not fit.
|
|
776
|
+
* A render failure degrades to an empty plan at the effective ceiling
|
|
777
|
+
* rather than throwing into the route/SSE path.
|
|
778
|
+
*/
|
|
779
|
+
const sectionPlan = () => {
|
|
780
|
+
try {
|
|
781
|
+
return planInjection(state.data)
|
|
782
|
+
} catch (err) {
|
|
783
|
+
console.error('[instruction-memory] render failed:', err)
|
|
784
|
+
return { text: '', budget: clampBudget(state.data.budgetChars), activeCount: 0, included: [], omitted: [] }
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
728
788
|
/**
|
|
729
789
|
* The thunk handed to the harness.
|
|
730
790
|
*
|
|
@@ -1287,7 +1347,8 @@ export function apply(ctx) {
|
|
|
1287
1347
|
/* ---------------- snapshots ---------------- */
|
|
1288
1348
|
|
|
1289
1349
|
const snapshot = () => {
|
|
1290
|
-
const
|
|
1350
|
+
const plan = sectionPlan()
|
|
1351
|
+
const preview = plan.text
|
|
1291
1352
|
return {
|
|
1292
1353
|
data: {
|
|
1293
1354
|
version: 1,
|
|
@@ -1310,6 +1371,16 @@ export function apply(ctx) {
|
|
|
1310
1371
|
available: true,
|
|
1311
1372
|
error: state.sectionError,
|
|
1312
1373
|
chars: preview.length,
|
|
1374
|
+
/**
|
|
1375
|
+
* The budget decision behind `preview`, for the settings page's
|
|
1376
|
+
* usage meter and per-entry marks. `budget` is the effective ceiling;
|
|
1377
|
+
* `included`/`omitted` carry one item per active entry in
|
|
1378
|
+
* consideration order; disabled or blank entries appear in neither.
|
|
1379
|
+
*/
|
|
1380
|
+
budget: plan.budget,
|
|
1381
|
+
activeCount: plan.activeCount,
|
|
1382
|
+
included: plan.included.map((item) => ({ id: item.id, chars: item.chars, truncated: item.truncated === true })),
|
|
1383
|
+
omitted: plan.omitted.map((item) => ({ id: item.id, chars: item.chars })),
|
|
1313
1384
|
/**
|
|
1314
1385
|
* How many times the harness has pulled this section into a prompt since
|
|
1315
1386
|
* boot. 0 means "prepared, never assembled" — the distinction the old
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-instruction-memory",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "User-only instruction memory for DSH: long-term instructions maintained in the settings UI and auto-injected into every conversation — the model has no write access. 指令记忆:只由用户在设置页维护的长期指令,自动注入此后每轮对话;模型没有任何写入口。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|