dsh-instruction-memory 1.0.10 → 1.0.11
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 +35 -3
- package/lib/index.js +91 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,22 @@ DSH 就会得到第二份空记忆,看起来像"记忆消失"。
|
|
|
85
85
|
导出的文件一定能被自己导回**——早先写死的 1,000,000 小于满配导出(200 × 6000 = 1.2 M 字符),
|
|
86
86
|
导致满配记忆导出后无法导入。
|
|
87
87
|
|
|
88
|
+
### 首次运行不写文件(惰性创建)
|
|
89
|
+
|
|
90
|
+
全新安装、尚无任何记忆时,插件**不会**在宿主启动那一刻创建空的 `memory.json`。
|
|
91
|
+
|
|
92
|
+
> 这里曾主动物化一份空存储,只为让设置页有个真实路径可显示。但那意味着插件在**进程启动、
|
|
93
|
+
> 零会话、零任务**的状态下执行了一次磁盘写入——这正是行为审计中唯一能被正当称为
|
|
94
|
+
> "没活干还在动"的副作用。现在文件由**第一个真正需要它的动作**创建:一次保存、一次导入,
|
|
95
|
+
> 或设置页读取 `state`(面板打开时就会读)。
|
|
96
|
+
|
|
97
|
+
在这之前:`storage.path` 仍会显示路径,但 `storage.onDisk` 为 `false`,设置页显示
|
|
98
|
+
「尚未写入磁盘」(该文案本就存在),`state.injection.registered === false`,`pulls === 0`。
|
|
99
|
+
挂载日志相应为 `from=first run (no store on disk yet) onDisk=no`。
|
|
100
|
+
|
|
101
|
+
**不受影响**:从 `.bak` 恢复、以及从旧位置迁移——这两条分支只有在**磁盘上已有真实数据**时才会走,
|
|
102
|
+
它们仍然会立即写回。
|
|
103
|
+
|
|
88
104
|
## 无 Web 服务器时依然生效
|
|
89
105
|
|
|
90
106
|
`systemPrompt` 是本插件唯一的硬依赖。**Web 服务器不是前置条件**:设置页那部分路由通过
|
|
@@ -103,11 +119,27 @@ DSH 就会得到第二份空记忆,看起来像"记忆消失"。
|
|
|
103
119
|
`info`,过程细节写 `debug`(走宿主的 `ctx.logger`):
|
|
104
120
|
|
|
105
121
|
```
|
|
106
|
-
[instruction-memory] mounted: store=<DSH_HOME>\instruction-memory\memory.json from=memory.json entries=2 enabled=true
|
|
122
|
+
[instruction-memory] mounted: store=<DSH_HOME>\instruction-memory\memory.json from=memory.json onDisk=yes entries=2 enabled=true section=437 chars (pending assembly) pulls=0
|
|
107
123
|
```
|
|
108
124
|
|
|
109
|
-
挂载行含**数据来源**(`memory.json` / `memory.json.bak (main file missing)` / `
|
|
110
|
-
/ `first run (
|
|
125
|
+
挂载行含**数据来源**(`memory.json` / `memory.json.bak (main file missing)` / `memory.json.bak (main file corrupt)`
|
|
126
|
+
/ `legacy cwd file (migrated)` / `first run (no store on disk yet)`)、**是否已在磁盘上**(`onDisk`)、
|
|
127
|
+
条目数、总开关状态、段落就绪时的字符数,以及 `pulls`。
|
|
128
|
+
|
|
129
|
+
### 为什么是 `section=… (pending assembly)` 而不是 `injected=…`
|
|
130
|
+
|
|
131
|
+
**「段落已就绪」≠「已经注入」。** 注入发生在宿主装配提示词时;插件在挂载那一刻**什么都没注入过**,
|
|
132
|
+
打印 `injected=437 chars` 会让人误以为记忆已经进入了某个提示词。这是本插件此前一处会误导排查的措辞。
|
|
133
|
+
|
|
134
|
+
真正的"到达了提示词"的信号是 **`pulls`**:宿主每次对这段做装配,插件注册的那个 thunk 就被求值一次,
|
|
135
|
+
计数器加一。因此
|
|
136
|
+
|
|
137
|
+
- `pulls=0` = 已准备、**从未装配**(进程刚起来、或会话空闲);
|
|
138
|
+
- `pulls=N` = 已被拉进提示词 N 次。
|
|
139
|
+
|
|
140
|
+
插件看不到会话或轮次,**不会**报告 `sessions=` 之类的数字——那会是编造。它只报告自己能真正观测到的东西。
|
|
141
|
+
`pulls` 同时出现在路由的 `state` 快照里(`injection.pulls`)。每次拉取另有一条 `debug` 行:
|
|
142
|
+
`section pulled into a prompt (pull #N)`。
|
|
111
143
|
|
|
112
144
|
**这是纯日志,绝不影响注入内容**:同一份存储在"有 logger"和"没有 logger"下渲染出的文本
|
|
113
145
|
**逐字节相同**(`verify.mjs` 有断言锁定这一点)。`debug` 级别由宿主日志级别过滤;
|
package/lib/index.js
CHANGED
|
@@ -437,9 +437,31 @@ export function apply(ctx) {
|
|
|
437
437
|
migratedFrom: null,
|
|
438
438
|
sectionRegistered: false,
|
|
439
439
|
sectionError: null,
|
|
440
|
+
/** Whether a store file is actually present on disk (diagnostics + the panel). */
|
|
441
|
+
storeExists: false,
|
|
440
442
|
}
|
|
441
443
|
let sectionDispose = null
|
|
442
444
|
|
|
445
|
+
/**
|
|
446
|
+
* A first run deliberately writes NOTHING (see readFromDisk step 3). This flag
|
|
447
|
+
* remembers that the store has not been materialised yet, so the file is
|
|
448
|
+
* created by the first action that genuinely needs it — a save, or the
|
|
449
|
+
* settings page asking for `state` — instead of by the bare act of the host
|
|
450
|
+
* booting with no session and no task.
|
|
451
|
+
*/
|
|
452
|
+
let needsMaterialize = false
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* How many times the host has pulled this section into a prompt, since boot.
|
|
456
|
+
*
|
|
457
|
+
* The plugin cannot see sessions or turns; it only knows that the harness
|
|
458
|
+
* evaluated the thunk it registered. That is still the honest answer to "did
|
|
459
|
+
* this actually reach a prompt?", and it is the thing the previous log line
|
|
460
|
+
* could not distinguish: a registered section with pulls=0 has been prepared
|
|
461
|
+
* but never injected.
|
|
462
|
+
*/
|
|
463
|
+
let sectionPulls = 0
|
|
464
|
+
|
|
443
465
|
const describeError = (err) => {
|
|
444
466
|
if (err === null || err === undefined) return '未知错误'
|
|
445
467
|
if (typeof err === 'string') return err
|
|
@@ -506,6 +528,21 @@ export function apply(ctx) {
|
|
|
506
528
|
}
|
|
507
529
|
}
|
|
508
530
|
|
|
531
|
+
/**
|
|
532
|
+
* The thunk handed to the harness.
|
|
533
|
+
*
|
|
534
|
+
* It is deliberately NOT `sectionText` itself: wrapping it lets the plugin
|
|
535
|
+
* count the pulls the host performs (`assembled`), which is the only
|
|
536
|
+
* first-hand evidence of "this reached a prompt" available on this side of the
|
|
537
|
+
* API. The returned text is untouched — the render stays byte-identical
|
|
538
|
+
* (verify.mjs pins that with and without a logger attached).
|
|
539
|
+
*/
|
|
540
|
+
const assembledSectionText = () => {
|
|
541
|
+
sectionPulls += 1
|
|
542
|
+
logDebug('section pulled into a prompt (pull #' + sectionPulls + ')')
|
|
543
|
+
return sectionText()
|
|
544
|
+
}
|
|
545
|
+
|
|
509
546
|
/**
|
|
510
547
|
* Keep the registered section exactly in step with the stored data: present
|
|
511
548
|
* when something must be injected, absent otherwise. An empty memory must
|
|
@@ -515,7 +552,7 @@ export function apply(ctx) {
|
|
|
515
552
|
const wanted = sectionText() !== ''
|
|
516
553
|
if (wanted && sectionDispose === null) {
|
|
517
554
|
try {
|
|
518
|
-
sectionDispose = systemPrompt.section({ name: SECTION_NAME, order: SECTION_ORDER, text:
|
|
555
|
+
sectionDispose = systemPrompt.section({ name: SECTION_NAME, order: SECTION_ORDER, text: assembledSectionText })
|
|
519
556
|
state.sectionRegistered = true
|
|
520
557
|
state.sectionError = null
|
|
521
558
|
logDebug('prompt section registered: order=' + SECTION_ORDER + ' chars=' + sectionText().length)
|
|
@@ -603,6 +640,10 @@ export function apply(ctx) {
|
|
|
603
640
|
state.storagePath = storePath
|
|
604
641
|
state.storageDisplay = storePath
|
|
605
642
|
state.storageError = null
|
|
643
|
+
state.storeExists = true
|
|
644
|
+
// A successful write is the one thing that makes the file real, so it is
|
|
645
|
+
// also what clears the "not materialised yet" flag set by a first run.
|
|
646
|
+
needsMaterialize = false
|
|
606
647
|
state.savedAt = Date.now()
|
|
607
648
|
logDebug('store written: ' + storePath + ' bytes=' + text.length
|
|
608
649
|
+ ' entries=' + state.data.entries.length
|
|
@@ -624,6 +665,7 @@ export function apply(ctx) {
|
|
|
624
665
|
state.storageDisplay = storePath
|
|
625
666
|
state.storageWarning = null
|
|
626
667
|
state.loadedFrom = null
|
|
668
|
+
state.storeExists = false
|
|
627
669
|
|
|
628
670
|
// 1. The canonical location under the harness home.
|
|
629
671
|
try {
|
|
@@ -633,6 +675,7 @@ export function apply(ctx) {
|
|
|
633
675
|
state.data = loaded.data
|
|
634
676
|
state.storageWarning = droppedEntriesMessage(loaded.dropped)
|
|
635
677
|
state.loadedFrom = 'memory.json'
|
|
678
|
+
state.storeExists = true
|
|
636
679
|
}
|
|
637
680
|
state.storageError = null
|
|
638
681
|
return
|
|
@@ -714,10 +757,31 @@ export function apply(ctx) {
|
|
|
714
757
|
if (err instanceof SyntaxError) state.storageError = '旧位置文件解析失败,已忽略:' + describeError(err)
|
|
715
758
|
}
|
|
716
759
|
|
|
717
|
-
// 3. First run:
|
|
718
|
-
//
|
|
719
|
-
|
|
720
|
-
|
|
760
|
+
// 3. First run: there is no store anywhere. Deliberately write NOTHING.
|
|
761
|
+
//
|
|
762
|
+
// This used to materialise an empty memory.json so the settings page had
|
|
763
|
+
// a real path to show. That made the plugin perform a disk write at
|
|
764
|
+
// process boot — i.e. with no session, no turn and no task — which is the
|
|
765
|
+
// one side effect that a behaviour audit can legitimately call "running
|
|
766
|
+
// with nothing to do". The file is now created by the first action that
|
|
767
|
+
// genuinely needs it: a save, an import, or the settings page reading
|
|
768
|
+
// `state` (see ensureStoreMaterialized). Until then the panel shows
|
|
769
|
+
// "尚未写入磁盘", a state its UI already renders.
|
|
770
|
+
needsMaterialize = true
|
|
771
|
+
state.loadedFrom = 'first run (no store on disk yet)'
|
|
772
|
+
state.storeExists = false
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Write the empty store, once, the first time something actually needs the
|
|
777
|
+
* file to exist. A failed attempt leaves the flag set so the next caller
|
|
778
|
+
* retries instead of silently pretending the file is there.
|
|
779
|
+
*
|
|
780
|
+
* @returns whether a write was performed and succeeded.
|
|
781
|
+
*/
|
|
782
|
+
const ensureStoreMaterialized = async () => {
|
|
783
|
+
if (!needsMaterialize) return false
|
|
784
|
+
return writeToDisk()
|
|
721
785
|
}
|
|
722
786
|
|
|
723
787
|
/* ---------------- boot ---------------- */
|
|
@@ -733,12 +797,17 @@ export function apply(ctx) {
|
|
|
733
797
|
syncSection()
|
|
734
798
|
// The one always-on line: it answers "is memory mounted, where does it
|
|
735
799
|
// live, and how much is being injected?" without any further digging.
|
|
800
|
+
// 措辞很重要:`section=` 说的是"这个段落已就绪、若被装配会产出多少字符",
|
|
801
|
+
// 而不是"已经注入"。在零会话时打印 `injected=` 会让人误以为记忆已经进入
|
|
802
|
+
// 了某个提示词。真正的注入证据是 pulls:宿主每拉取一次加一(初始必为 0)。
|
|
736
803
|
const chars = sectionText().length
|
|
737
804
|
logInfo('mounted: store=' + storePath
|
|
738
805
|
+ ' from=' + String(state.loadedFrom)
|
|
806
|
+
+ ' onDisk=' + (state.storeExists ? 'yes' : 'no')
|
|
739
807
|
+ ' entries=' + state.data.entries.length
|
|
740
808
|
+ ' enabled=' + (state.data.enabled === true)
|
|
741
|
-
+ '
|
|
809
|
+
+ ' section=' + (chars > 0 ? chars + ' chars (pending assembly)' : 'nothing to inject')
|
|
810
|
+
+ ' pulls=' + sectionPulls
|
|
742
811
|
+ (state.storageError !== null ? ' error=' + state.storageError : ''))
|
|
743
812
|
})
|
|
744
813
|
|
|
@@ -774,12 +843,20 @@ export function apply(ctx) {
|
|
|
774
843
|
warning: state.storageWarning !== null ? state.storageWarning : state.backupError,
|
|
775
844
|
savedAt: state.savedAt,
|
|
776
845
|
migratedFrom: state.migratedFrom,
|
|
846
|
+
/** false until the store file has actually been written (see readFromDisk step 3). */
|
|
847
|
+
onDisk: state.storeExists,
|
|
777
848
|
},
|
|
778
849
|
injection: {
|
|
779
850
|
registered: state.sectionRegistered,
|
|
780
851
|
available: true,
|
|
781
852
|
error: state.sectionError,
|
|
782
853
|
chars: preview.length,
|
|
854
|
+
/**
|
|
855
|
+
* How many times the harness has pulled this section into a prompt since
|
|
856
|
+
* boot. 0 means "prepared, never assembled" — the distinction the old
|
|
857
|
+
* `injected=` log line could not make.
|
|
858
|
+
*/
|
|
859
|
+
pulls: sectionPulls,
|
|
783
860
|
},
|
|
784
861
|
preview,
|
|
785
862
|
limits: { minBudget: MIN_BUDGET, maxBudget: MAX_BUDGET, maxEntries: MAX_ENTRIES },
|
|
@@ -954,10 +1031,17 @@ export function apply(ctx) {
|
|
|
954
1031
|
const dispatch = async (method, args) => {
|
|
955
1032
|
await boot
|
|
956
1033
|
switch (method) {
|
|
957
|
-
case 'state':
|
|
1034
|
+
case 'state': {
|
|
1035
|
+
// The settings page is the first thing that needs a real file on disk —
|
|
1036
|
+
// it displays the absolute path, and the user may then hand-edit it.
|
|
1037
|
+
// A first run writes nothing at boot (readFromDisk step 3), so the
|
|
1038
|
+
// materialisation happens here, on a deliberate user action, instead of
|
|
1039
|
+
// as a side effect of the host merely starting up.
|
|
1040
|
+
await ensureStoreMaterialized()
|
|
958
1041
|
// Same envelope as every other method. One response shape means the
|
|
959
1042
|
// Client can never again be handed a payload it silently ignores.
|
|
960
1043
|
return { ok: true, saved: true, message: null, snapshot: snapshot() }
|
|
1044
|
+
}
|
|
961
1045
|
case 'save-entry':
|
|
962
1046
|
return saveEntry(args !== null && typeof args === 'object' ? args.entry : null)
|
|
963
1047
|
case 'delete-entry':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-instruction-memory",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
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",
|