sage-mem 1.9.1 → 1.9.2
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 +10 -2
- package/README.zh.md +10 -2
- package/lib/autodream/config.js +10 -0
- package/lib/autodream/engine.js +4 -0
- package/lib/autodream/llm-loop.js +77 -10
- package/lib/autodream/report.js +20 -10
- package/lib/autodream/settings.js +6 -0
- package/lib/client.js +10 -0
- package/lib/typert.host.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
**Cross-session memory for DeepSeek Harness — plain markdown files, no database, no worker.**
|
|
14
14
|
|
|
15
|
-
**Current release: `1.9.
|
|
15
|
+
**Current release: `1.9.2`** — the npm badge above is the dynamic source of truth. **Contract baseline: `1.0.0`** — the first stable release, whose public commitments, the capability boundaries and the list of things it explicitly cannot do are frozen in the
|
|
16
16
|
[frozen contract](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md).
|
|
17
17
|
|
|
18
18
|
If it is useful to you, a Star is the most direct feedback.
|
|
@@ -293,7 +293,7 @@ The guard has two paths, with **different** coverage, spelled out:
|
|
|
293
293
|
| **The plugin itself** writing (autodream consolidation) | The caller passes the version it read at write time | A pending record `<state directory>/conflicts/<id>.json` is written, keeping **the version on disk** and **the version it wanted to write** — with only one side there is nothing to judge |
|
|
294
294
|
|
|
295
295
|
- **Conflicts are not merged automatically**; which side to keep is your decision (the record is JSON, open it directly when you need to). Silent retry and silent abandonment both decide on your behalf.
|
|
296
|
-
- **The injection side does not read this queue yet**: a memory with an unresolved conflict is still injected as body text. The `conflict_pending` suppression rule arrives with **
|
|
296
|
+
- **The injection side does not read this queue yet**: a memory with an unresolved conflict is still injected as body text. The `conflict_pending` suppression rule arrives with **2.0.0 conflict detection**, together with the panel entry that resolves conflicts — wiring the suppression without that entry would make a memory **permanently non-injectable**, which is worse than the current state. For now, open `conflicts/<id>.json` and set `status` to `resolved` or `dismissed` (or delete the file).
|
|
297
297
|
- **The same conflict is not duplicated**: if a failure matches an unresolved record field for field (same file, both bodies, both versions), no extra copy is appended.
|
|
298
298
|
- **The host file sandbox**: autodream runs **outside any session**, so it cannot inherit a session workspace. On a `workspace-write` host the plugin passes a per-call sandbox policy **narrowed to the memory directory** (narrower than the fallback root); on a `read-only` host it passes nothing, the write is still refused, and the run is recorded as a **failure** (not "success, nothing changed").
|
|
299
299
|
- Known boundary: **editing memories in a shell does not go through this path**. The write gate guards "memory writes that go through the tools", not "all modifications to files"; redirection and the like inside `pwsh` are not recognized. If it cannot be sure, it does not claim it — so that "cannot see it" is not mistakenly recorded as fact.
|
|
@@ -344,6 +344,14 @@ Consolidation is a background batch job and need not be the same model as the on
|
|
|
344
344
|
- The panel always shows **which one is actually in effect**; if the route named in the configuration happens to disappear from the catalog, the dropdown still shows it rather than quietly jumping back to the default.
|
|
345
345
|
- When the model catalog cannot be obtained (host differences), it degrades to typing `provider` and `model` by hand.
|
|
346
346
|
|
|
347
|
+
### When the Report Does Not Fit
|
|
348
|
+
|
|
349
|
+
The consolidation report is written by the model in one go, and a single output has a length cap. **The cap, the retry and the truncated label** all have defined behaviour:
|
|
350
|
+
|
|
351
|
+
- **The single-output cap is configurable**: Settings → Autodream → Settings → Advanced → "single output cap" (default 32768 tokens, range 1024–131072). For reasoning models the reasoning and the body **share** that budget; when the closing turn spends it all on reasoning, no body is left — raise the cap and run again.
|
|
352
|
+
- **Retry when the body is empty**: if an output hits the cap with **no body at all** (reasoning only), one retry asks for "the body directly, do not re-analyse"; up to 2 times, and only then is the report marked truncated. When there is a partial body, the loop asks it to continue from where it stopped (up to 3 rounds).
|
|
353
|
+
- **Truncation is stated**: the report's first screen, the consolidation manifest, the machine-readable record and the Settings → Autodream "last run" line all say "report truncated (fragment)" — a half report is never reported as "success". If the closing step produced no text, the report says "the model gave no textual summary" instead of reusing narration from an earlier step.
|
|
354
|
+
|
|
347
355
|
### Rollback Points
|
|
348
356
|
|
|
349
357
|
**Whenever this run is a "direct rewrite", a rollback point is created before work starts** (a full snapshot of the top-level `.md` files in the memory directory, each with a sha256), and after the run you can see it in the settings page and roll back to it in one click.
|
package/README.zh.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
> File-based cross-session memory for DeepSeek Harness — every memory is a plain markdown file you can open, edit and delete. No database, no worker, no port.
|
|
16
16
|
|
|
17
|
-
**当前发布版本:`1.9.
|
|
17
|
+
**当前发布版本:`1.9.2`** —— 顶部 npm 徽章是动态权威。**契约基线:`1.0.0`** —— 第一个稳定版,公开承诺、能力边界与「明确做不到」的清单冻结在
|
|
18
18
|
[契约冻结](https://github.com/gezi-wen/sage-mem/blob/main/docs/1.0.0-contract.md)。
|
|
19
19
|
|
|
20
20
|
如果它对你有用,给个 Star 是最直接的反馈。
|
|
@@ -293,7 +293,7 @@ sage-mem 给记忆目录的写入加了**版本守卫**:写入前记下当前
|
|
|
293
293
|
| **插件自己**写(自动做梦的整理) | 调用方在写入时带上读到的版本 | 落一条待处理记录 `<状态目录>/conflicts/<id>.json`,里面留着**盘上那一版**和**想要写的那一版** —— 只有一边等于没得判 |
|
|
294
294
|
|
|
295
295
|
- **冲突不会自动合并**,保留哪边由你决定(记录是 JSON,需要时直接打开看)。静默重试和静默放弃都是在替你决定。
|
|
296
|
-
- **注入侧目前不读这个队列**:有未裁决冲突的记忆照常作为正文注入;`conflict_pending` 这条抑制规则要等 **
|
|
296
|
+
- **注入侧目前不读这个队列**:有未裁决冲突的记忆照常作为正文注入;`conflict_pending` 这条抑制规则要等 **2.0.0 的矛盾检测**(连面板里的裁决入口一起)才生效 —— 只接抑制、不做入口,会让一条记忆**永久不再被注入**,比现在更糟。现阶段处理:打开 `conflicts/<id>.json`,把 `status` 改成 `resolved` 或 `dismissed`(或直接删掉文件)。
|
|
297
297
|
- **同一件事不会越积越多**:一次失败如果与队列里某条未裁决记录逐字段相同(同一文件、两份正文、两个版本),不再追加副本。
|
|
298
298
|
- **宿主的文件沙箱**:自动做梦在**会话外**运行,拿不到会话工作区。宿主用 `workspace-write` 时,插件给每次写入带上**收窄到记忆目录**的逐次沙箱策略(比回退根更窄);宿主是 `read-only` 时不传策略、写入照旧被拒,且这一趟会被记成**失败**(不是「成功,没改什么」)。
|
|
299
299
|
- 已知边界:**在 shell 里改记忆不走这条路**。写门守的是「经过工具的记忆写入」,不是「所有对文件的修改」;`pwsh` 里的重定向之类认不出来。认不准就不认,免得把「看不到」误记成事实。
|
|
@@ -344,6 +344,14 @@ sage-mem 给记忆目录的写入加了**版本守卫**:写入前记下当前
|
|
|
344
344
|
- 面板下方始终显示**实际生效的是哪条**;配置里那条路线万一从目录里消失了,下拉框也会把它显示出来而不是悄悄跳回默认。
|
|
345
345
|
- 拿不到模型目录时(宿主差异),退化成手填 `provider` 与 `model`。
|
|
346
346
|
|
|
347
|
+
### 报告写不完的时候
|
|
348
|
+
|
|
349
|
+
整理报告由模型一口气写完,而单次输出有长度上限。**上限、重试、残篇标注**三件事都有明确行为:
|
|
350
|
+
|
|
351
|
+
- **单次输出上限可配**:设置页「自动做梦 → 设置 → 高级设置 → 单次输出上限」(默认 32768 token,范围 1024–131072)。思考型模型的思考与正文**共用**这份额度;收尾那轮思考吃满时正文会一个字不剩 —— 这类报告先把上限调大再跑。
|
|
352
|
+
- **零正文先重试**:一轮额度用尽却**没有任何正文**(只剩思考)时,先要求它「直接输出正文、不要重新分析」重试;最多 2 次,仍为零才标残篇。撞上限但**有正文**的,按「从中断处继续写」续写(最多 3 轮)。
|
|
353
|
+
- **残篇会被指出来**:报告首屏、整理声明、机读记录与设置页「上次运行」那一行都会标「报告被截断(残篇)」—— 不会再把半篇报成「成功」。收尾那一步没写文字时,报告写「模型没有给出文字说明」,**不拿中途旁白顶替**。
|
|
354
|
+
|
|
347
355
|
### 回滚点
|
|
348
356
|
|
|
349
357
|
**只要这次是「直接改写」,开工前就先建一个回滚点**(记忆目录顶层 `.md` 全量快照,每条带 sha256),跑完之后能在设置页看到它、也能一键退回去。
|
package/lib/autodream/config.js
CHANGED
|
@@ -19,6 +19,16 @@ export const DEFAULT_CONFIG = {
|
|
|
19
19
|
provider: '',
|
|
20
20
|
model: '',
|
|
21
21
|
maxSteps: 24,
|
|
22
|
+
/**
|
|
23
|
+
* 单次模型调用的输出上限(token)。
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ 2026-10-26(1.9.2)从写死的 4096 提到 32768 并做成设置项。4096 对**思考型模型**
|
|
26
|
+
* 不够:思考与正文共用同一份额度,收尾那一轮里思考吃光额度、正文一个字都没剩下,
|
|
27
|
+
* 结果报告是残篇而面板报「成功」(2026-10-25 两趟手动整理连续复现)。
|
|
28
|
+
* 调大它不是治本(报告长度没有上界),治本是「零正文先重试、写不完才标残篇」;
|
|
29
|
+
* 做成设置项是为了让用户按自己模型的输出上限调,而不是每次为它改代码。
|
|
30
|
+
*/
|
|
31
|
+
maxOutputTokens: 32768,
|
|
22
32
|
maxSnapshotKeep: 5,
|
|
23
33
|
/** 回滚按钮的默认范围:files = 只恢复该次运行动过的文件;all = 整目录恢复到该时点。 */
|
|
24
34
|
rollbackScope: 'files',
|
package/lib/autodream/engine.js
CHANGED
|
@@ -316,6 +316,7 @@ export class AutodreamEngine {
|
|
|
316
316
|
auditAfter,
|
|
317
317
|
finalText: result.finalText,
|
|
318
318
|
continuations: result.continuations,
|
|
319
|
+
retries: result.retries,
|
|
319
320
|
truncated: result.truncated,
|
|
320
321
|
archiveMode: policy.mode,
|
|
321
322
|
archiveRequested: policy.requested,
|
|
@@ -344,6 +345,7 @@ export class AutodreamEngine {
|
|
|
344
345
|
auditAfter,
|
|
345
346
|
finalText: result.finalText,
|
|
346
347
|
continuations: result.continuations,
|
|
348
|
+
retries: result.retries,
|
|
347
349
|
truncated: result.truncated,
|
|
348
350
|
transcript: result.transcript,
|
|
349
351
|
tokensIn: result.tokensIn,
|
|
@@ -388,6 +390,7 @@ export class AutodreamEngine {
|
|
|
388
390
|
tokensOut: result.tokensOut,
|
|
389
391
|
// 残篇要能在界面上被认出来(面板读 lastResult):只报「成功」是不够的。
|
|
390
392
|
continuations: result.continuations,
|
|
393
|
+
retries: result.retries,
|
|
391
394
|
truncated: result.truncated,
|
|
392
395
|
report: report.name,
|
|
393
396
|
snapshotId: snapshot?.id ?? null,
|
|
@@ -403,6 +406,7 @@ export class AutodreamEngine {
|
|
|
403
406
|
changeCount: result.changes.length,
|
|
404
407
|
snapshotId: snapshot?.id ?? null,
|
|
405
408
|
continuations: result.continuations,
|
|
409
|
+
retries: result.retries,
|
|
406
410
|
truncated: result.truncated,
|
|
407
411
|
auditBefore,
|
|
408
412
|
auditAfter,
|
|
@@ -151,18 +151,42 @@ export async function streamOnce(llm, req) {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
|
-
*
|
|
154
|
+
* 一次模型调用的输出上限(配置缺字段 / 坏值时的回落值)。
|
|
155
155
|
*
|
|
156
156
|
* ⚠️ **调大它解决不了「长报告被截断」**:报告长度没有上界,调大只是把墙往后挪一格
|
|
157
157
|
* (2026-10-17 实测:10-01 的报告 5532 字符、10-13 是 5957,都完整收尾;这次 8553 字符
|
|
158
158
|
* 就断在半个句子里)。真正的处理是撞上它时**接着写** —— 见下面的 MAX_CONTINUATIONS。
|
|
159
|
+
*
|
|
160
|
+
* 但 4096 对**思考型模型**连「把收尾那一轮写完」都不够:思考与正文共用额度,
|
|
161
|
+
* 2026-10-25 两趟手动整理都在最后一步思考吃光额度、正文零字(1.9.2 改成可配置,默认 32768)。
|
|
159
162
|
*/
|
|
160
|
-
const
|
|
163
|
+
const FALLBACK_MAX_TOKENS = 32768
|
|
164
|
+
|
|
165
|
+
/** 配置里的单次输出上限。缺字段(老配置 / 测试桩)或坏值都回落到常量。 */
|
|
166
|
+
export function resolveMaxTokens(cfg) {
|
|
167
|
+
const n = Number(cfg?.maxOutputTokens)
|
|
168
|
+
return Number.isFinite(n) && n >= 1024 ? Math.round(n) : FALLBACK_MAX_TOKENS
|
|
169
|
+
}
|
|
170
|
+
|
|
161
171
|
/** 撞上单次上限后最多续写几轮:够把最后一段写完,又不至于让一次跑飞掉。 */
|
|
162
172
|
const MAX_CONTINUATIONS = 3
|
|
173
|
+
/**
|
|
174
|
+
* 「零正文截断」最多重试几次(1.9.2)。
|
|
175
|
+
*
|
|
176
|
+
* 零正文不是「这段写完了」,而是**思考把额度吃光了**:直接标残篇会让一次本来能写完的报告
|
|
177
|
+
* 白丢。先重试一次「直接输出正文、不要重新分析」;仍为零才标残篇。
|
|
178
|
+
* 上限 2:思考型模型可能连续两轮都只顾着思考,再多就是拿 token 赌运气。
|
|
179
|
+
*/
|
|
180
|
+
const MAX_EMPTY_RETRIES = 2
|
|
163
181
|
/** 续写指令。三条禁令缺一不可 —— 重写整篇 / 重新开头 / 道歉寒暄,模型都干得出来。 */
|
|
164
182
|
const CONTINUE_PROMPT =
|
|
165
183
|
'(上一条回答被输出长度上限截断了。请**从中断处继续写**,不要重复已经写过的内容,不要重新开头,不要解释或道歉,直接接着写。)'
|
|
184
|
+
/**
|
|
185
|
+
* 零正文重试指令。与 CONTINUE_PROMPT 的区别:**没有「已写内容」可续**,
|
|
186
|
+
* 上一轮只有思考。所以这里要压住「再分析一遍」的冲动,只要正文。
|
|
187
|
+
*/
|
|
188
|
+
const EMPTY_RETRY_PROMPT =
|
|
189
|
+
'(上一条回答把输出额度全用在思考上了,最终没有任何正文。请**直接输出正文**:不要再重新分析、不要重复思考过程、不要解释,直接给出这一轮要交付的内容。)'
|
|
166
190
|
|
|
167
191
|
/** 取这一次调用里的全部文字块,拼成一段。 */
|
|
168
192
|
function textOf(asm) {
|
|
@@ -206,8 +230,12 @@ export async function agentLoop(cfg, runOpts) {
|
|
|
206
230
|
// 截断与续写的账:`continuations` 是「确实接着写过几轮」,`truncated` 是「续写用尽还是没写完」。
|
|
207
231
|
// 两个都要进声明与报告 —— 「报告是残篇却报成功」正是这一版要修的那处静默。
|
|
208
232
|
let continuations = 0
|
|
233
|
+
/** 零正文重试的账(1.9.2):与「续写」分开记 —— 一个是接着写,一个是根本没写出来。 */
|
|
234
|
+
let retries = 0
|
|
209
235
|
let truncated = false
|
|
210
236
|
let lastStep = 0
|
|
237
|
+
// 每次调用都从这里取上限:配置改了不用重启(下一趟就用新值),老配置缺字段也能跑。
|
|
238
|
+
const maxTokensPerCall = resolveMaxTokens(cfg)
|
|
211
239
|
const allToolsUsed = []
|
|
212
240
|
const addUsage = (u) => {
|
|
213
241
|
if (!u) return
|
|
@@ -228,7 +256,7 @@ export async function agentLoop(cfg, runOpts) {
|
|
|
228
256
|
system,
|
|
229
257
|
messages,
|
|
230
258
|
tools: toolSchemas,
|
|
231
|
-
maxTokens:
|
|
259
|
+
maxTokens: maxTokensPerCall,
|
|
232
260
|
signal,
|
|
233
261
|
})
|
|
234
262
|
addUsage(asm.usage)
|
|
@@ -237,30 +265,64 @@ export async function agentLoop(cfg, runOpts) {
|
|
|
237
265
|
let text = ''
|
|
238
266
|
let calls = []
|
|
239
267
|
let segment = 0
|
|
268
|
+
/** 下一段是不是「零正文重试」来的 —— 只影响过程清单上的标记。 */
|
|
269
|
+
let retryNext = false
|
|
240
270
|
for (;;) {
|
|
271
|
+
const isRetry = retryNext
|
|
272
|
+
retryNext = false
|
|
241
273
|
const segText = textOf(asm)
|
|
242
274
|
const blocks = asm.blocks()
|
|
243
275
|
const segCalls = blocks.filter((b) => b.type === 'tool-call')
|
|
244
276
|
// 只有第一段可能带工具调用:被截断时 BlockAssembler 已经丢掉了参数残缺的 tool-call。
|
|
245
277
|
if (segment === 0) calls = segCalls
|
|
246
278
|
text += segText
|
|
279
|
+
const segMark = segment === 0 ? {} : isRetry ? { retry: true } : { continuation: true }
|
|
247
280
|
transcript.push({
|
|
248
281
|
step,
|
|
249
282
|
text: segText,
|
|
250
283
|
tools: segCalls.map((c) => c.name),
|
|
251
|
-
...
|
|
284
|
+
...segMark,
|
|
252
285
|
})
|
|
253
286
|
this.stepLog.push({
|
|
254
287
|
step,
|
|
255
288
|
tools: segCalls.map((c) => c.name),
|
|
256
289
|
chars: segText.length,
|
|
257
290
|
at: Date.now(),
|
|
258
|
-
...
|
|
291
|
+
...segMark,
|
|
259
292
|
})
|
|
260
293
|
segment += 1
|
|
261
294
|
if (asm.finish.kind !== 'max-tokens') break
|
|
262
|
-
|
|
263
|
-
|
|
295
|
+
/**
|
|
296
|
+
* 零正文的截断:不是「这段写完了」,而是**思考把额度吃光了**(思考型模型常见)。
|
|
297
|
+
* 1.9.2 起先重试「直接输出正文」;重试用尽仍无正文才标残篇。
|
|
298
|
+
* 旧行为(1.0.1)是直接标残篇 —— 那会让一次本来能写完的报告白丢一段收尾。
|
|
299
|
+
*/
|
|
300
|
+
if (!segText) {
|
|
301
|
+
if (retries >= MAX_EMPTY_RETRIES) {
|
|
302
|
+
truncated = true
|
|
303
|
+
break
|
|
304
|
+
}
|
|
305
|
+
retries += 1
|
|
306
|
+
// 把这一轮原样回灌(通常只有 reasoning 块):让模型看到它上一轮只思考、没交付。
|
|
307
|
+
// 完全空的 blocks 不回灌 —— 空的 assistant 消息不是合法输入,宁可不给。
|
|
308
|
+
if (blocks.length) messages.push(assistantMessage(blocks, provider, model))
|
|
309
|
+
messages.push(userMessage(EMPTY_RETRY_PROMPT))
|
|
310
|
+
retryNext = true
|
|
311
|
+
this.phase = `第 ${step} 轮:零正文被截断,重试直接输出正文 ${retries}/${MAX_EMPTY_RETRIES}`
|
|
312
|
+
asm = await this.streamOnce(llm, {
|
|
313
|
+
provider,
|
|
314
|
+
model,
|
|
315
|
+
system,
|
|
316
|
+
messages,
|
|
317
|
+
tools: toolSchemas,
|
|
318
|
+
maxTokens: maxTokensPerCall,
|
|
319
|
+
signal,
|
|
320
|
+
})
|
|
321
|
+
addUsage(asm.usage)
|
|
322
|
+
continue
|
|
323
|
+
}
|
|
324
|
+
// 有正文但被截断:接着写。名额用尽就如实标残篇。
|
|
325
|
+
if (continuations >= MAX_CONTINUATIONS) {
|
|
264
326
|
truncated = true
|
|
265
327
|
break
|
|
266
328
|
}
|
|
@@ -274,13 +336,15 @@ export async function agentLoop(cfg, runOpts) {
|
|
|
274
336
|
system,
|
|
275
337
|
messages,
|
|
276
338
|
tools: toolSchemas,
|
|
277
|
-
maxTokens:
|
|
339
|
+
maxTokens: maxTokensPerCall,
|
|
278
340
|
signal,
|
|
279
341
|
})
|
|
280
342
|
addUsage(asm.usage)
|
|
281
343
|
}
|
|
282
344
|
|
|
283
|
-
|
|
345
|
+
// ④ **每一步都覆盖**(不是「有字才覆盖」):最后一步零正文时,收尾说明必须留空,
|
|
346
|
+
// 不能把中途某轮的旁白当成「模型的收尾说明」写进报告(2026-10-25 残篇报告的伴随症状)。
|
|
347
|
+
finalText = text
|
|
284
348
|
if (asm.finish.kind === 'error') {
|
|
285
349
|
throw new Error(`模型返回错误:${asm.finish.failure?.message ?? '未知'}`)
|
|
286
350
|
}
|
|
@@ -308,9 +372,10 @@ export async function agentLoop(cfg, runOpts) {
|
|
|
308
372
|
|
|
309
373
|
if (truncated) {
|
|
310
374
|
// 过程清单要以「没写完」收尾:否则报告里那半句话看起来只是模型话说到一半。
|
|
375
|
+
const retryNote = retries ? `(其中零正文重试 ${retries} 次)` : ''
|
|
311
376
|
transcript.push({
|
|
312
377
|
step: lastStep,
|
|
313
|
-
text: `(输出撞上单次长度上限,续写 ${continuations}
|
|
378
|
+
text: `(输出撞上单次长度上限,续写 ${continuations} 轮${retryNote}后仍未写完 —— 上面这段是残篇,后文缺失)`,
|
|
314
379
|
tools: [],
|
|
315
380
|
})
|
|
316
381
|
}
|
|
@@ -321,6 +386,8 @@ export async function agentLoop(cfg, runOpts) {
|
|
|
321
386
|
tokensIn,
|
|
322
387
|
tokensOut,
|
|
323
388
|
continuations,
|
|
389
|
+
/** 零正文重试次数(1.9.2):进报告与机读记录,让「重试过」和「一次写成」分得开。 */
|
|
390
|
+
retries,
|
|
324
391
|
truncated,
|
|
325
392
|
toolsUsed: allToolsUsed,
|
|
326
393
|
touched: runner.touchedFiles(),
|
package/lib/autodream/report.js
CHANGED
|
@@ -57,6 +57,8 @@ export async function writeDeclaration(payload) {
|
|
|
57
57
|
finalText: payload.finalText ?? '',
|
|
58
58
|
// 1.0.1:输出撞上单次上限这件事要进机读记录 —— 残篇在界面上和「话说一半」长得一模一样。
|
|
59
59
|
continuations: Number(payload.continuations) || 0,
|
|
60
|
+
// 1.9.2:零正文重试是另一本账 —— 「接着写过」与「根本没写出来」不能混成一个数字。
|
|
61
|
+
retries: Number(payload.retries) || 0,
|
|
60
62
|
truncated: payload.truncated === true,
|
|
61
63
|
rolledBackAt: 0,
|
|
62
64
|
rolledBackAtHuman: '',
|
|
@@ -79,10 +81,14 @@ export function renderDeclaration(m, headingLevel = 1) {
|
|
|
79
81
|
: '- 回滚点:未做(只出报告模式不写盘,无需回滚)',
|
|
80
82
|
`- 审计:结构问题 ${m.audit.before.problems} → ${m.audit.after.problems}`,
|
|
81
83
|
...(m.truncated
|
|
82
|
-
? [
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
? [
|
|
85
|
+
`- ⚠️ 输出被单次长度上限截断:续写 ${m.continuations ?? 0} 轮${m.retries ? `、零正文重试 ${m.retries} 次` : ''}后仍未写完,收尾说明是**残篇**`,
|
|
86
|
+
]
|
|
87
|
+
: m.retries
|
|
88
|
+
? [`- 输出曾出现零正文截断(思考吃满额度),重试 ${m.retries} 次后补齐`]
|
|
89
|
+
: m.continuations
|
|
90
|
+
? [`- 输出曾撞上单次长度上限,已自动续写 ${m.continuations} 轮补齐`]
|
|
91
|
+
: []),
|
|
86
92
|
'',
|
|
87
93
|
`${H}# 改了什么、为什么`,
|
|
88
94
|
'',
|
|
@@ -213,12 +219,15 @@ export async function writeReport(payload) {
|
|
|
213
219
|
*/
|
|
214
220
|
const truncLines = payload.truncated
|
|
215
221
|
? [
|
|
216
|
-
`- ⚠️ **本次输出被单次长度上限截断**:自动续写 ${payload.continuations ?? 0}
|
|
222
|
+
`- ⚠️ **本次输出被单次长度上限截断**:自动续写 ${payload.continuations ?? 0} 轮` +
|
|
223
|
+
`${payload.retries ? `、零正文重试 ${payload.retries} 次` : ''}后仍未写完 —— ` +
|
|
217
224
|
'下面「模型的收尾说明」是**残篇**,后文缺失',
|
|
218
225
|
]
|
|
219
|
-
: payload.
|
|
220
|
-
? [`-
|
|
221
|
-
:
|
|
226
|
+
: payload.retries
|
|
227
|
+
? [`- 输出曾出现零正文截断(思考吃满额度),重试 ${payload.retries} 次后补齐(报告完整)`]
|
|
228
|
+
: payload.continuations
|
|
229
|
+
? [`- 输出曾撞上单次长度上限,已自动续写 ${payload.continuations} 轮补齐(报告完整)`]
|
|
230
|
+
: []
|
|
222
231
|
|
|
223
232
|
const lines = [
|
|
224
233
|
`# Autodream 报告 · ${stampHuman(payload.startedAt)}`,
|
|
@@ -257,8 +266,9 @@ export async function writeReport(payload) {
|
|
|
257
266
|
'## 过程',
|
|
258
267
|
'',
|
|
259
268
|
...payload.transcript.map((t) => {
|
|
260
|
-
//
|
|
261
|
-
|
|
269
|
+
// 「续写」与「零正文重试」都必须标出来:否则同一轮出现好几条,读起来像模型重复劳动。
|
|
270
|
+
// 两者分开标 —— 一个是「接着写」,一个是「上一段根本没写出来」,读者要看得出区别。
|
|
271
|
+
const mark = t.retry ? '(零正文重试)' : t.continuation ? '(截断续写)' : ''
|
|
262
272
|
const kind = t.tools.length ? `:调用 ${t.tools.join('、')}` : ':结束'
|
|
263
273
|
// ⚠️ **先压平空白再截断**。模型这一步的文字里常带换行与 `## 标题`,直接 slice(0,120)
|
|
264
274
|
// 会把标题漏成真标题 —— 报告读起来像「内容重复了一遍」(2026-10-17 10:42 那次真踩到:
|
|
@@ -99,6 +99,12 @@ export async function setConfig(patch) {
|
|
|
99
99
|
if (p.minHours !== undefined) c.minHours = clampNum(Number(p.minHours), 1, 720, DEFAULT_CONFIG.minHours)
|
|
100
100
|
if (p.minSessions !== undefined) c.minSessions = Math.round(clampNum(Number(p.minSessions), 1, 200, DEFAULT_CONFIG.minSessions))
|
|
101
101
|
if (p.maxSteps !== undefined) c.maxSteps = Math.round(clampNum(Number(p.maxSteps), 1, 80, DEFAULT_CONFIG.maxSteps))
|
|
102
|
+
/**
|
|
103
|
+
* 单次输出上限(1.9.2)。范围 1024–131072:下限够小、上限够到大多数模型的硬顶;
|
|
104
|
+
* 越界按统一口径**夹到合法范围**,不写一份坏配置。
|
|
105
|
+
*/
|
|
106
|
+
if (p.maxOutputTokens !== undefined)
|
|
107
|
+
c.maxOutputTokens = Math.round(clampNum(Number(p.maxOutputTokens), 1024, 131072, DEFAULT_CONFIG.maxOutputTokens))
|
|
102
108
|
if (p.maxSnapshotKeep !== undefined)
|
|
103
109
|
c.maxSnapshotKeep = Math.round(clampNum(Number(p.maxSnapshotKeep), 1, 50, DEFAULT_CONFIG.maxSnapshotKeep))
|
|
104
110
|
if (typeof p.provider === 'string') c.provider = p.provider.trim().slice(0, 120)
|
package/lib/client.js
CHANGED
|
@@ -3954,6 +3954,10 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
3954
3954
|
" · 结构问题 " + last.problemsBefore + " → " + last.problemsAfter +
|
|
3955
3955
|
" · token " + last.tokensIn + " / " + last.tokensOut)
|
|
3956
3956
|
: ("失败:" + (last.error || "未知"))) +
|
|
3957
|
+
// 1.9.2:残篇必须在状态行上就能看见 —— 只写「成功」正是 10-25 那次误判的来源。
|
|
3958
|
+
(last.ok && last.truncated ? " · ⚠ 报告被截断(残篇)" : "") +
|
|
3959
|
+
(last.ok && !last.truncated && last.retries ? (" · 零正文重试 " + last.retries + " 次后补齐") : "") +
|
|
3960
|
+
(last.continuations ? (" · 续写 " + last.continuations + " 轮") : "") +
|
|
3957
3961
|
(last.report ? " · 报告 " + last.report : ""))
|
|
3958
3962
|
: null,
|
|
3959
3963
|
]),
|
|
@@ -4068,6 +4072,12 @@ h("span", { key: "sep", className: "smem-actions-sep", "aria-hidden": "true" }),
|
|
|
4068
4072
|
h("input", { className: "smem-num", key: "v", type: "number", min: "1", max: "80", value: c.maxSteps, onChange: function (e) { patch({ maxSteps: Number(e.target.value) }); } }),
|
|
4069
4073
|
h("span", { className: "smem-status", key: "s" }, "一轮 = 一次模型调用 + 它要的工具调用"),
|
|
4070
4074
|
]),
|
|
4075
|
+
// 1.9.2:单次输出上限(原写死 4096)。思考型模型下 4096 会让收尾轮只剩思考、零正文。
|
|
4076
|
+
h("div", { className: "smem-autodream-row", key: "maxout" }, [
|
|
4077
|
+
h("span", { className: "smem-autodream-key", key: "k" }, "单次输出上限"),
|
|
4078
|
+
h("input", { className: "smem-num", key: "v", type: "number", min: "1024", max: "131072", value: c.maxOutputTokens, onChange: function (e) { patch({ maxOutputTokens: Number(e.target.value) }); } }),
|
|
4079
|
+
h("span", { className: "smem-status", key: "s" }, "token(思考与正文共用;报告残篇时先调它)"),
|
|
4080
|
+
]),
|
|
4071
4081
|
h("div", { className: "smem-autodream-row", key: "keep" }, [
|
|
4072
4082
|
h("span", { className: "smem-autodream-key", key: "k" }, "保留快照"),
|
|
4073
4083
|
h("input", { className: "smem-num", key: "v", type: "number", min: "1", max: "50", value: c.maxSnapshotKeep, onChange: function (e) { patch({ maxSnapshotKeep: Number(e.target.value) }); } }),
|
package/lib/typert.host.js
CHANGED
|
@@ -307,6 +307,8 @@ const autodreamConfigSchema = z.object({
|
|
|
307
307
|
'provider': z.string().readonly(),
|
|
308
308
|
'model': z.string().readonly(),
|
|
309
309
|
'maxSteps': z.number().readonly(),
|
|
310
|
+
// 1.9.2:单次输出上限(token)。响应与 patch 两侧都必须声明 —— strict codec 会剥掉未声明字段。
|
|
311
|
+
'maxOutputTokens': z.number().readonly(),
|
|
310
312
|
'maxSnapshotKeep': z.number().readonly(),
|
|
311
313
|
'rollbackScope': z.string().readonly(),
|
|
312
314
|
/**
|
|
@@ -414,6 +416,7 @@ const autodreamPatchSchema = z.object({
|
|
|
414
416
|
'provider': z.string().optional(),
|
|
415
417
|
'model': z.string().optional(),
|
|
416
418
|
'maxSteps': z.number().optional(),
|
|
419
|
+
'maxOutputTokens': z.number().optional(),
|
|
417
420
|
'maxSnapshotKeep': z.number().optional(),
|
|
418
421
|
'rollbackScope': z.string().optional(),
|
|
419
422
|
/**
|
package/package.json
CHANGED