mocode-ai 1.7.6 → 1.7.7
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.
|
@@ -22,6 +22,13 @@ const ATTACHMENT_TAIL = '. Analyze the attached image content directly.';
|
|
|
22
22
|
const UNKNOWN_FRAME_NAME = 'frame';
|
|
23
23
|
/** 恢复提示只写进**第一条**被剪占位,避免 N 条占位重复同一句话侵蚀 token。 */
|
|
24
24
|
const RECOVERY_HINT = ' Call computer{screenshot} if you need the current screen.';
|
|
25
|
+
/**
|
|
26
|
+
* 帧被剪枝后占位行的固定前缀(见 pruneFrameMessage,完整行为 `[screenshot omitted: <帧名> · step N]`)。
|
|
27
|
+
* 剪枝会把合成附件消息整个 content 换成占位(前言 ATTACHMENT_PREAMBLE 随之消失),
|
|
28
|
+
* rollback 对齐真实用户轮时必须认出这个占位、不把它算成一轮——那里因循环依赖无法
|
|
29
|
+
* import 本模块,故在 rollback/store.ts 内联了同一行格式(按整行正则判定),修改此串时两处必须同步。
|
|
30
|
+
*/
|
|
31
|
+
const PRUNED_FRAME_PREFIX = '[screenshot omitted: ';
|
|
25
32
|
/**
|
|
26
33
|
* 白名单:参与滑动窗口的屏幕帧名前缀。
|
|
27
34
|
*
|
|
@@ -139,11 +146,11 @@ function pruneFrameMessage(message, names, step, withHint) {
|
|
|
139
146
|
for (const part of parts) {
|
|
140
147
|
if (part.type !== 'image_url')
|
|
141
148
|
continue;
|
|
142
|
-
lines.push(
|
|
149
|
+
lines.push(`${PRUNED_FRAME_PREFIX}${names[index] ?? UNKNOWN_FRAME_NAME} · step ${step}]`);
|
|
143
150
|
index++;
|
|
144
151
|
}
|
|
145
152
|
if (lines.length === 0)
|
|
146
|
-
lines.push(
|
|
153
|
+
lines.push(`${PRUNED_FRAME_PREFIX}${names[0] ?? UNKNOWN_FRAME_NAME} · step ${step}]`);
|
|
147
154
|
if (withHint)
|
|
148
155
|
lines[0] = `${lines[0]}${RECOVERY_HINT}`;
|
|
149
156
|
const content = [{ type: 'text', text: lines.join('\n') }];
|
package/dist/repl/runtime.js
CHANGED
|
@@ -352,12 +352,6 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
352
352
|
layout.contentWrite(`${ui.dim}(没有可回滚的轮次)${ui.reset}\n`);
|
|
353
353
|
return;
|
|
354
354
|
}
|
|
355
|
-
// 各轮 user 全文(预填用;按 user 消息顺序与 turnList 对齐)
|
|
356
|
-
const userTexts = [];
|
|
357
|
-
for (const m of history) {
|
|
358
|
-
if (m.role === 'user')
|
|
359
|
-
userTexts.push(textOf(m.content));
|
|
360
|
-
}
|
|
361
355
|
const items = turnList.map((t) => ({ firstLine: t.firstLine }));
|
|
362
356
|
let picked;
|
|
363
357
|
try {
|
|
@@ -368,8 +362,11 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
368
362
|
}
|
|
369
363
|
if (picked === null)
|
|
370
364
|
return; // Esc 取消
|
|
371
|
-
// picked(0-based)= 第 (picked+1) 轮:删该轮及之后(planRollback(picked)
|
|
372
|
-
|
|
365
|
+
// picked(0-based)= 第 (picked+1) 轮:删该轮及之后(planRollback(picked) 保前 picked 轮),
|
|
366
|
+
// 预填该轮 user 输入。必须经 store 按"尾部对齐的真实用户轮消息"定位——直接按 history 里
|
|
367
|
+
// role==='user' 数下标会把工具附件的合成 user 消息/被剪枝老轮算进去,预填错轮。
|
|
368
|
+
const pickedMessage = runtime.rollback.turnUserMessage(picked, history);
|
|
369
|
+
const prefillText = pickedMessage ? textOf(pickedMessage.content) : '';
|
|
373
370
|
const plan = runtime.rollback.plan(picked, history);
|
|
374
371
|
// 清屏(擦选轮菜单 + /rollback 回显)+ 复位 lastView(dim 空),给文件询问一个干净、resize 安全的画面
|
|
375
372
|
layout.clearContent();
|
package/dist/rollback/store.js
CHANGED
|
@@ -47,6 +47,61 @@ const EXCLUDED_WORKSPACE_DIRS = Object.freeze([
|
|
|
47
47
|
'.ruff_cache',
|
|
48
48
|
'.gradle',
|
|
49
49
|
]);
|
|
50
|
+
/**
|
|
51
|
+
* 工具经 modelAttachments 注入的合成 user 消息前言(见 agent/tool-turn.ts 的
|
|
52
|
+
* ATTACHMENT_PREAMBLE)。这类消息 role 也是 'user',但不对应 beginTurn 的真实用户轮,
|
|
53
|
+
* 回滚按 role==='user' 数轮次会与 turns 数组错位,导致"回退一条却连带删掉好几轮"。
|
|
54
|
+
* 两处常量必须保持一致;不 import tool-turn 是为避免 rollback→agent 的层级反向依赖。
|
|
55
|
+
*/
|
|
56
|
+
const TOOL_ATTACHMENT_PREAMBLE = 'The read_file tool loaded the following visual input: ';
|
|
57
|
+
/**
|
|
58
|
+
* 视觉滑动窗口剪枝占位的机器固定格式(必须与 context/vision-window.ts 的
|
|
59
|
+
* pruneFrameMessage 输出逐字一致):一行或多行 `[screenshot omitted: <帧名> · step <数字>]`,
|
|
60
|
+
* 首行可能再跟固定恢复提示后缀。用整串正则而不是前缀匹配,避免把恰好以
|
|
61
|
+
* "[screenshot omitted: " 开头的真实用户输入误判成占位。
|
|
62
|
+
*
|
|
63
|
+
* 这里内联而不是 import vision-window:vision-window → llm → … → rollback/index 会形成
|
|
64
|
+
* 循环依赖,在共享进程的测试里触发 "Cannot access 'defaultRollbackStore' before initialization"。
|
|
65
|
+
*/
|
|
66
|
+
const PRUNED_FRAME_LINE_RE = /^\[screenshot omitted: .+? · step \d+\]$/;
|
|
67
|
+
const PRUNED_FRAME_HINT_SUFFIX = ' Call computer{screenshot} if you need the current screen.';
|
|
68
|
+
function isPrunedFrameText(text) {
|
|
69
|
+
if (typeof text !== 'string')
|
|
70
|
+
return false;
|
|
71
|
+
const lines = text.split('\n');
|
|
72
|
+
return lines.every((line, i) => {
|
|
73
|
+
const core = i === 0 && line.endsWith(PRUNED_FRAME_HINT_SUFFIX) ? line.slice(0, -PRUNED_FRAME_HINT_SUFFIX.length) : line;
|
|
74
|
+
return PRUNED_FRAME_LINE_RE.test(core);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 判断一条 user 消息是否对应一个真实用户轮(= beginTurn 记进 turns 的那一条)。
|
|
79
|
+
*
|
|
80
|
+
* 排除两种合成 user 消息(都是 role:'user' 但不计轮次):
|
|
81
|
+
* 1. 工具批次提交时由 modelAttachments 注入的附件消息:ContentPart[] 的 text part
|
|
82
|
+
* 以工具附件前言开头;
|
|
83
|
+
* 2. 上述附件消息经视觉滑动窗口剪枝后的占位:整个 content 被替换成
|
|
84
|
+
* "[screenshot omitted: …]" 文本,前言已不存在(见 vision-window.pruneFrameMessage),
|
|
85
|
+
* 形态可能是数组 text part,也可能是纯字符串。
|
|
86
|
+
*
|
|
87
|
+
* 用户自己发的多模态消息没有这两个标记,不会被误判。
|
|
88
|
+
*/
|
|
89
|
+
function isRealTurnUserMessage(message) {
|
|
90
|
+
if (message.role !== 'user')
|
|
91
|
+
return false;
|
|
92
|
+
const content = message.content;
|
|
93
|
+
if (typeof content === 'string')
|
|
94
|
+
return !isPrunedFrameText(content);
|
|
95
|
+
if (!Array.isArray(content))
|
|
96
|
+
return false;
|
|
97
|
+
for (const part of content) {
|
|
98
|
+
if (part && typeof part === 'object' && part.type === 'text' && typeof part.text === 'string') {
|
|
99
|
+
if (part.text.startsWith(TOOL_ATTACHMENT_PREAMBLE) || isPrunedFrameText(part.text))
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
50
105
|
const CAPTURE_FILE_LIMIT = 1024 * 1024;
|
|
51
106
|
const CAPTURE_TOTAL_LIMIT = 32 * 1024 * 1024;
|
|
52
107
|
const CAPTURE_ENTRY_LIMIT = 20000;
|
|
@@ -440,6 +495,32 @@ export class RollbackStore {
|
|
|
440
495
|
listTurns() {
|
|
441
496
|
return this.turns.slice();
|
|
442
497
|
}
|
|
498
|
+
/**
|
|
499
|
+
* 取菜单第 pickedIndex(0-based)轮对应的真实用户消息(用于 /rollback 后预填)。
|
|
500
|
+
* 与 findCutoffIndex 同一套尾对齐规则。无对应消息(续接历史缺轮)时返回 undefined。
|
|
501
|
+
*/
|
|
502
|
+
turnUserMessage(pickedIndex, history) {
|
|
503
|
+
const index = this.findTurnUserIndex(pickedIndex, history);
|
|
504
|
+
return index < 0 ? undefined : history[index];
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* 菜单第 pickedIndex(0-based)轮对应的真实用户消息在 history 中的下标。
|
|
508
|
+
* turns 是真实轮次的尾部连续子集(滚动剪枝/压缩只丢旧轮),故菜单第 k 轮 =
|
|
509
|
+
* history 中倒数第 (turns.length-k) 条真实用户轮消息。找不到返回 -1。
|
|
510
|
+
*/
|
|
511
|
+
findTurnUserIndex(pickedIndex, history) {
|
|
512
|
+
let wantReverseRank = this.turns.length - pickedIndex;
|
|
513
|
+
if (wantReverseRank <= 0)
|
|
514
|
+
return -1;
|
|
515
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
516
|
+
if (!isRealTurnUserMessage(history[i]))
|
|
517
|
+
continue;
|
|
518
|
+
wantReverseRank -= 1;
|
|
519
|
+
if (wantReverseRank === 0)
|
|
520
|
+
return i;
|
|
521
|
+
}
|
|
522
|
+
return -1;
|
|
523
|
+
}
|
|
443
524
|
/**
|
|
444
525
|
* 只读侧门(供 /diff 渲染):返回 turnId > sinceTurnId 的全部快照,浅拷贝数组。
|
|
445
526
|
* 快照内容含 before 原文,调用方不得 mutate 返回值;用于展示,不影响回滚语义。
|
|
@@ -447,19 +528,21 @@ export class RollbackStore {
|
|
|
447
528
|
snapshotsSince(sinceTurnId) {
|
|
448
529
|
return this.snapshots.filter((snapshot) => snapshot.turnId > sinceTurnId);
|
|
449
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* 回滚切点:保留菜单前 n 轮,从第 n+1 轮(菜单下标 n,0-based)的真实 user 消息处切断。
|
|
533
|
+
*
|
|
534
|
+
* turns(菜单)= 当前仍可回滚的轮次,是真实轮次的**尾部连续子集**(滚动剪枝/压缩只丢旧轮),
|
|
535
|
+
* 故从尾部倒数定位,而不是正数第 n 条——压缩的「user 逐字通道」会把更老的 user 原文
|
|
536
|
+
* 捞回保留区,正数下标与 turns 不再连续对齐。找不到(无第 n+1 轮)时返回 history.length。
|
|
537
|
+
*/
|
|
450
538
|
findCutoffIndex(n, history) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (history[i].role === 'user') {
|
|
454
|
-
seen += 1;
|
|
455
|
-
if (seen === n + 1)
|
|
456
|
-
return i;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return history.length;
|
|
539
|
+
const index = this.findTurnUserIndex(n, history);
|
|
540
|
+
return index < 0 ? history.length : index;
|
|
460
541
|
}
|
|
461
542
|
planRollback(n, history) {
|
|
462
543
|
const cutoffTurnId = this.turns[n - 1]?.turnId ?? 0;
|
|
544
|
+
// turns 与 history 真实 user 消息尾对齐后再取切点:滚动剪枝(>50 轮)/压缩/续接后
|
|
545
|
+
// turns 只是尾部子集,不能再把 n 当成全局第 n 条 user 消息,否则会多删 offset 轮。
|
|
463
546
|
const cutoffIndex = this.findCutoffIndex(n, history);
|
|
464
547
|
const order = [];
|
|
465
548
|
const map = new Map();
|
|
@@ -587,7 +670,9 @@ export class RollbackStore {
|
|
|
587
670
|
return { deletedMsgs, revertedFiles, conflictedFiles };
|
|
588
671
|
}
|
|
589
672
|
pruneAfterCompaction(history) {
|
|
590
|
-
|
|
673
|
+
// 只数真实用户轮(排除工具附件合成 user 消息);压缩会整体丢弃旧轮,
|
|
674
|
+
// 保留下来的总是最近的轮次,故 turns 同样从尾部对齐截取。
|
|
675
|
+
const count = history.filter((message) => isRealTurnUserMessage(message)).length;
|
|
591
676
|
this.turns = count >= this.turns.length ? this.turns : this.turns.slice(-count);
|
|
592
677
|
const alive = new Set(this.turns.map((turn) => turn.turnId));
|
|
593
678
|
this.snapshots = this.snapshots.filter((snapshot) => alive.has(snapshot.turnId));
|
|
@@ -603,7 +688,9 @@ export class RollbackStore {
|
|
|
603
688
|
rebuildFromHistory(history) {
|
|
604
689
|
const rebuilt = [];
|
|
605
690
|
for (const message of history) {
|
|
606
|
-
|
|
691
|
+
// 跳过工具附件注入的合成 user 消息,否则 turns 比真实轮次多,
|
|
692
|
+
// /rollback 菜单与 history 切点(planRollback 按 turns[n-1])整体错位。
|
|
693
|
+
if (!isRealTurnUserMessage(message))
|
|
607
694
|
continue;
|
|
608
695
|
const first = toText(message.content).split('\n')[0] ?? '';
|
|
609
696
|
rebuilt.push({ turnId: rebuilt.length + 1, firstLine: truncateDisplay(first, 40) });
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -90,6 +90,10 @@ export class Runtime {
|
|
|
90
90
|
this.assertOpen();
|
|
91
91
|
return this.context.rollbackStore.listTurns();
|
|
92
92
|
},
|
|
93
|
+
turnUserMessage: (pickedIndex, history) => {
|
|
94
|
+
this.assertOpen();
|
|
95
|
+
return this.context.rollbackStore.turnUserMessage(pickedIndex, history);
|
|
96
|
+
},
|
|
93
97
|
plan: (n, history) => {
|
|
94
98
|
this.assertIdle('plan rollback');
|
|
95
99
|
const plan = this.context.rollbackStore.planRollback(n, history);
|