@kkelly-offical/kkcode 0.1.6 → 0.1.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.
@@ -457,6 +457,7 @@ export async function runHybridLongAgent({
457
457
  "### Preview Findings", previewFindings.slice(0, 2000), "",
458
458
  "### Blueprint Architecture", architectureText.slice(0, 3000)
459
459
  ].join("\n")
460
+ const seenFilePaths = new Set() // #3 去重:跨阶段文件路径去重
460
461
 
461
462
  let codingRollbackCount = 0
462
463
  const maxCodingRollbacks = Number(hybridConfig.max_coding_rollbacks || 2)
@@ -499,10 +500,17 @@ export async function runHybridLongAgent({
499
500
  stage.tasks.map(t => [t.taskId, taskProgress[t.taskId]]).filter(([, v]) => Boolean(v))
500
501
  )
501
502
 
503
+ // #4 计划锚点 — 每阶段动态构建,不存入 priorContext 避免被压缩掉
504
+ const stageStatuses = stagePlan.stages.map((s, i) => {
505
+ const marker = i < stageIndex ? "✓" : i === stageIndex ? "→" : " "
506
+ return `[${marker}] 阶段${i + 1}: ${s.name || s.stageId}`
507
+ }).join("\n")
508
+ const planAnchor = `## 计划锚点\n目标: ${stagePlan.objective || prompt}\n进度: ${stageIndex + 1}/${stagePlan.stages.length}\n${stageStatuses}\n\n`
509
+
502
510
  const stageResult = await runStageBarrier({
503
511
  stage, sessionId, config: configState.config, model, providerType,
504
512
  seedTaskProgress: seeded, objective: prompt,
505
- stageIndex, stageCount: stagePlan.stages.length, priorContext,
513
+ stageIndex, stageCount: stagePlan.stages.length, priorContext: planAnchor + priorContext,
506
514
  stuckTracker,
507
515
  onTaskComplete: async (taskData) => {
508
516
  await saveTaskCheckpoint(sessionId, taskData.stageId, taskData.taskId, taskData)
@@ -536,12 +544,19 @@ export async function runHybridLongAgent({
536
544
  successCount: stageResult.successCount, failCount: stageResult.failCount
537
545
  }
538
546
 
539
- // 知识传递
547
+ // #1 阶段级压缩 + #3 文件去重 — 结构化摘要,跨阶段去重文件路径
540
548
  const taskSummaries = Object.values(stageResult.taskProgress || {})
541
549
  .filter(t => t.lastReply)
542
- .map(t => `[${t.taskId}] ${t.status}: ${t.lastReply.slice(0, 300)}`)
543
- if (taskSummaries.length) {
544
- priorContext += `\n### Stage ${stageIndex + 1}: ${stage.name || stage.stageId} (${stageResult.allSuccess ? "PASS" : "FAIL"})\n${taskSummaries.join("\n")}\n`
550
+ .map(t => ` - [${t.taskId}] ${t.status}: ${t.lastReply.slice(0, 250)}`)
551
+ const stageFiles = (stageResult.fileChanges || [])
552
+ .map(f => (typeof f === "string" ? f : (f.path || f.file || "")))
553
+ .filter(Boolean)
554
+ const newFiles = stageFiles.filter(f => !seenFilePaths.has(f))
555
+ newFiles.forEach(f => seenFilePaths.add(f))
556
+ if (taskSummaries.length || newFiles.length) {
557
+ const fileNote = newFiles.length ? `\n 新增/修改文件: ${newFiles.join(", ")}` : ""
558
+ const failNote = !stageResult.allSuccess ? ` 失败任务数: ${stageResult.failCount}` : ""
559
+ priorContext += `\n### 阶段${stageIndex + 1}: ${stage.name || stage.stageId} (${stageResult.allSuccess ? "PASS" : "FAIL"}${failNote})\n${taskSummaries.join("\n")}${fileNote}\n`
545
560
  }
546
561
  // #4 TaskBus 注入到 priorContext
547
562
  if (taskBus) {
@@ -665,6 +680,7 @@ export async function runHybridLongAgent({
665
680
  }
666
681
 
667
682
  stageIndex++
683
+ recoveryCount = 0 // reset per-stage recovery counter after successful stage
668
684
  await saveCheckpoint(sessionId, { name: `hybrid_stage_${stage.stageId}`, iteration, currentPhase, stageIndex, stagePlan, taskProgress, planFrozen, lastProgress })
669
685
  }
670
686
 
@@ -357,6 +357,7 @@ async function runParallelLongAgent({
357
357
  // --- End L1.5 ---
358
358
 
359
359
  let priorContext = ""
360
+ const seenFilePaths = new Set() // #3 去重:跨阶段文件路径去重,避免 priorContext 重复提及
360
361
 
361
362
  while (stageIndex < stagePlan.stages.length) {
362
363
  const state = await LongAgentManager.get(sessionId)
@@ -411,6 +412,13 @@ async function runParallelLongAgent({
411
412
  .filter(([, value]) => Boolean(value))
412
413
  )
413
414
 
415
+ // #4 计划锚点 — 每个阶段执行前重建,确保模型始终看到完整计划和当前进度
416
+ const stageStatuses = stagePlan.stages.map((s, i) => {
417
+ const marker = i < stageIndex ? "✓" : i === stageIndex ? "→" : " "
418
+ return `[${marker}] 阶段${i + 1}: ${s.name || s.stageId}`
419
+ }).join("\n")
420
+ const planAnchor = `## 计划锚点\n目标: ${stagePlan.objective || prompt}\n进度: ${stageIndex + 1}/${stagePlan.stages.length}\n${stageStatuses}\n\n`
421
+
414
422
  const stageResult = await runStageBarrier({
415
423
  stage,
416
424
  sessionId,
@@ -421,7 +429,7 @@ async function runParallelLongAgent({
421
429
  objective: prompt,
422
430
  stageIndex,
423
431
  stageCount: stagePlan.stages.length,
424
- priorContext
432
+ priorContext: planAnchor + priorContext
425
433
  })
426
434
 
427
435
  for (const [taskId, progress] of Object.entries(stageResult.taskProgress || {})) {
@@ -446,12 +454,19 @@ async function runParallelLongAgent({
446
454
  remainingFiles: stageResult.remainingFiles
447
455
  }
448
456
 
449
- // Build inter-stage knowledge transfer summary
457
+ // #1 阶段级压缩 + #3 去重 — 结构化阶段摘要,文件路径跨阶段去重
450
458
  const taskSummaries = Object.values(stageResult.taskProgress || {})
451
459
  .filter(t => t.lastReply)
452
- .map(t => `[${t.taskId}] ${t.status}: ${t.lastReply.slice(0, 300)}`)
453
- if (taskSummaries.length) {
454
- priorContext += `### Stage ${stageIndex + 1}: ${stage.name || stage.stageId} (${stageResult.allSuccess ? "PASS" : "FAIL"})\n${taskSummaries.join("\n")}\n\n`
460
+ .map(t => ` - [${t.taskId}] ${t.status}: ${t.lastReply.slice(0, 250)}`)
461
+ const stageFiles = (stageResult.fileChanges || [])
462
+ .map(f => (typeof f === "string" ? f : (f.path || f.file || "")))
463
+ .filter(Boolean)
464
+ const newFiles = stageFiles.filter(f => !seenFilePaths.has(f))
465
+ newFiles.forEach(f => seenFilePaths.add(f))
466
+ if (taskSummaries.length || newFiles.length) {
467
+ const fileNote = newFiles.length ? `\n 新增/修改文件: ${newFiles.join(", ")}` : ""
468
+ const failNote = !stageResult.allSuccess ? `\n 失败任务数: ${stageResult.failCount}` : ""
469
+ priorContext += `### 阶段${stageIndex + 1}: ${stage.name || stage.stageId} (${stageResult.allSuccess ? "PASS" : "FAIL"})${failNote}\n${taskSummaries.join("\n")}${fileNote}\n\n`
455
470
  }
456
471
 
457
472
  lastProgress = {
@@ -557,6 +572,7 @@ async function runParallelLongAgent({
557
572
  }
558
573
 
559
574
  stageIndex += 1
575
+ recoveryCount = 0 // reset per-stage recovery counter after successful stage
560
576
  // Always checkpoint after each stage for reliable recovery
561
577
  await saveCheckpoint(sessionId, {
562
578
  name: `stage_${stage.stageId}`,
@@ -29,8 +29,11 @@ import { saveCheckpoint } from "./checkpoint.mjs"
29
29
  import { askPlanApproval } from "../tool/question-prompt.mjs"
30
30
  import { createValidator } from "./task-validator.mjs"
31
31
 
32
+ // Max chars kept in active context per tool_result — process output beyond this is truncated
33
+ const TOOL_RESULT_ACTIVE_LIMIT = 3000
34
+
32
35
  const READ_ONLY_TOOLS = new Set([
33
- "read", "glob", "grep", "list", "webfetch", "websearch", "codesearch", "background_output", "todowrite", "enter_plan", "exit_plan"
36
+ "read", "glob", "grep", "list", "webfetch", "websearch", "codesearch", "background_output", "todowrite", "enter_plan"
34
37
  ])
35
38
 
36
39
  function addUsage(target, delta) {
@@ -534,9 +537,16 @@ export async function processTurnLoop({
534
537
  ? `\n被截断的工具调用: ${truncatedToolNames}。请完整重新发起这些工具调用。如果是创建大文件,使用 write(mode="append") 分段追加;如果是修改已有文件的局部内容,使用 patch 按行号范围替换。`
535
538
  : `\nTruncated tool calls: ${truncatedToolNames}. Re-issue these tool calls completely. For large file creation, use write(mode="append") to append in chunks. For modifying sections of existing files, use patch to replace by line range.`)
536
539
  : ""
540
+ // Anchor: last 200 chars of truncated text so model knows exactly where to resume
541
+ const textTail = response.text ? response.text.slice(-200) : ""
542
+ const anchorHint = textTail
543
+ ? (language === "zh"
544
+ ? `\n[锚点] 上次输出末尾:...${textTail}`
545
+ : `\n[Anchor] Last output ended with: ...${textTail}`)
546
+ : ""
537
547
  const continuePrompt = language === "zh"
538
- ? `[输出被截断 ${continueCount}/${MAX_CONTINUES}] 你的上一条回复在输出 token 上限处被截断。请从你停止的地方精确继续,不要重复已经写过的内容。如果你正在执行工具调用,请完整重新发起。${toolHint}`
539
- : `[OUTPUT TRUNCATED ${continueCount}/${MAX_CONTINUES}] Your previous response was cut off at the output token limit. Continue EXACTLY from where you stopped. Do not repeat any content you already wrote. If you were in the middle of a tool call, re-issue it completely.${toolHint}`
548
+ ? `[输出被截断 ${continueCount}/${MAX_CONTINUES}] 你的上一条回复在输出 token 上限处被截断。请从你停止的地方精确继续,不要重复已经写过的内容。如果你正在执行工具调用,请完整重新发起。${toolHint}${anchorHint}`
549
+ : `[OUTPUT TRUNCATED ${continueCount}/${MAX_CONTINUES}] Your previous response was cut off at the output token limit. Continue EXACTLY from where you stopped. Do not repeat any content you already wrote. If you were in the middle of a tool call, re-issue it completely.${toolHint}${anchorHint}`
540
550
  await appendMessage(sessionId, "user", continuePrompt,
541
551
  { mode, model, providerType, step, turnId, synthetic: true }
542
552
  )
@@ -642,42 +652,51 @@ export async function processTurnLoop({
642
652
  }
643
653
  }
644
654
 
645
- await PermissionEngine.check({
646
- config: configState.config,
647
- sessionId,
648
- tool: call.name,
649
- mode,
650
- pattern,
651
- command,
652
- risk,
653
- reason: `tool call from model at step ${step}`
654
- })
655
+ // Plan mode enforcement: block write tools when _planMode is active
656
+ if (toolContext._planMode && !READ_ONLY_TOOLS.has(call.name) && call.name !== "exit_plan") {
657
+ result = {
658
+ name: call.name,
659
+ status: "error",
660
+ output: `[PLAN MODE] Cannot execute '${call.name}' in plan mode. Finish your plan outline and call exit_plan to present it for user approval.`
661
+ }
662
+ } else {
663
+ await PermissionEngine.check({
664
+ config: configState.config,
665
+ sessionId,
666
+ tool: call.name,
667
+ mode,
668
+ pattern,
669
+ command,
670
+ risk,
671
+ reason: `tool call from model at step ${step}`
672
+ })
655
673
 
656
- const tool = await ToolRegistry.get(call.name)
657
- result = !tool
658
- ? {
659
- name: call.name,
660
- status: "error",
661
- output: `unknown tool: ${call.name}`,
662
- error: `unknown tool: ${call.name}`
663
- }
664
- : await executeTool({
665
- tool,
666
- args: call.args,
667
- sessionId,
668
- turnId,
669
- context: {
670
- cwd,
671
- mode,
672
- delegateTask,
673
- signal,
674
+ const tool = await ToolRegistry.get(call.name)
675
+ result = !tool
676
+ ? {
677
+ name: call.name,
678
+ status: "error",
679
+ output: `unknown tool: ${call.name}`,
680
+ error: `unknown tool: ${call.name}`
681
+ }
682
+ : await executeTool({
683
+ tool,
684
+ args: call.args,
674
685
  sessionId,
675
686
  turnId,
676
- config: configState.config,
677
- ...toolContext
678
- },
679
- signal
680
- })
687
+ context: {
688
+ cwd,
689
+ mode,
690
+ delegateTask,
691
+ signal,
692
+ sessionId,
693
+ turnId,
694
+ config: configState.config,
695
+ ...toolContext
696
+ },
697
+ signal
698
+ })
699
+ }
681
700
  } catch (error) {
682
701
  result = {
683
702
  name: call.name,
@@ -700,8 +719,10 @@ export async function processTurnLoop({
700
719
  result = {
701
720
  ...result,
702
721
  output: approval.approved
703
- ? "User APPROVED the plan. Proceed with implementation."
704
- : `User REJECTED the plan. Feedback: ${approval.feedback || "no feedback provided"}`,
722
+ ? "User APPROVED the plan. Proceed with implementation immediately."
723
+ : approval.requestChanges
724
+ ? `User requested changes to the plan. Feedback: ${approval.feedback || "no specific feedback"}. Revise your plan and call exit_plan again with the updated plan.`
725
+ : `User REJECTED the plan. Feedback: ${approval.feedback || "no feedback provided"}. Do not proceed — the plan has been cancelled.`,
705
726
  metadata: { ...result.metadata, planApprovalResult: approval }
706
727
  }
707
728
  }
@@ -798,15 +819,19 @@ export async function processTurnLoop({
798
819
  })
799
820
 
800
821
  // User message: tool_result blocks (one per tool call, in order)
822
+ // Process output beyond TOOL_RESULT_ACTIVE_LIMIT is truncated to keep context lean
801
823
  const resultContent = []
802
824
  for (const call of response.toolCalls) {
803
825
  const entry = callResults.get(call.id)
804
- const output = entry?.result?.output || ""
826
+ const rawOutput = entry?.result?.output || ""
805
827
  const isError = entry?.result?.status === "error"
828
+ const content = rawOutput.length > TOOL_RESULT_ACTIVE_LIMIT
829
+ ? `${rawOutput.slice(0, TOOL_RESULT_ACTIVE_LIMIT)}\n[...过程输出已截断,共 ${rawOutput.length} 字符,仅保留前 ${TOOL_RESULT_ACTIVE_LIMIT} 字符]`
830
+ : rawOutput
806
831
  resultContent.push({
807
832
  type: "tool_result",
808
833
  tool_use_id: call.id,
809
- content: output,
834
+ content,
810
835
  is_error: isError
811
836
  })
812
837
  }
@@ -0,0 +1,25 @@
1
+ Agent mode active. Full tool access enabled.
2
+
3
+ ## When to use plan mode first
4
+
5
+ Call enter_plan PROACTIVELY before making changes when ANY of these apply:
6
+ - Task requires changes to 3+ files
7
+ - Multiple valid implementation approaches exist
8
+ - Architectural decisions need user input
9
+ - The user hasn't specified implementation details
10
+ - The task involves refactoring, new features, or system-level changes
11
+
12
+ Simple tasks can proceed directly without planning:
13
+ - Single-file bug fix with clear solution
14
+ - Typo or minor text correction
15
+ - Adding one small function with clear requirements
16
+
17
+ ## Plan → Execute flow
18
+
19
+ 1. Call enter_plan (reason: why planning is needed)
20
+ 2. Explore the codebase to understand the context (read, glob, grep)
21
+ 3. Outline your complete plan in your response text
22
+ 4. Call exit_plan(plan, files) to present it for user approval
23
+ 5. If approved: proceed with implementation immediately
24
+ 6. If changes requested: revise and call exit_plan again
25
+ 7. If rejected: stop and ask the user what they want instead
@@ -1,9 +1,31 @@
1
- Plan mode active.
2
-
3
- Return:
4
- 1. goal summary
5
- 2. implementation steps
6
- 3. files to edit
7
- 4. validation checklist
8
-
9
- Do not execute mutation actions in this mode.
1
+ Plan mode active.
2
+
3
+ ## Your task
4
+ Create a detailed implementation plan. Do NOT execute any file mutations, bash commands, or write operations in this mode.
5
+
6
+ ## Required plan format
7
+
8
+ 1. **Goal**: Clear statement of what will be accomplished
9
+ 2. **Approach**: High-level strategy and key architectural decisions
10
+ 3. **Implementation steps**: Numbered, specific, actionable steps
11
+ 4. **Files to create/modify**: List each file with the specific change description
12
+ 5. **Validation checklist**: How to verify the implementation is correct (tests, syntax checks, manual steps)
13
+
14
+ ## Quality criteria
15
+ - Each step must be specific enough to execute without ambiguity
16
+ - Include file paths, function names, and line numbers where relevant
17
+ - Identify dependencies between steps (step N must complete before step M)
18
+ - Flag risks, trade-offs, or alternative approaches considered
19
+ - Estimate complexity: simple / medium / complex
20
+
21
+ ## Allowed actions in plan mode
22
+ - Read files (read, glob, grep)
23
+ - Search the web (websearch, codesearch)
24
+ - Explore the codebase to gather information
25
+
26
+ ## After creating your plan
27
+ Call exit_plan with the complete plan text and the list of files to modify.
28
+ The user will then choose:
29
+ - **Approve** → you proceed with implementation immediately
30
+ - **Request Changes** → you revise the plan and call exit_plan again
31
+ - **Reject** → the plan is cancelled, do not proceed
@@ -0,0 +1,196 @@
1
+ import { isGitRepo } from "../util/git.mjs"
2
+ import { getLatestGhostCommit, listGhostCommits } from "../storage/ghost-commit-store.mjs"
3
+ import { restoreGhostCommit } from "../util/git.mjs"
4
+ import { askQuestionInteractive } from "../tool/question-prompt.mjs"
5
+ import { EventBus } from "../core/events.mjs"
6
+ import { EVENT_TYPES } from "../core/constants.mjs"
7
+
8
+ /**
9
+ * 回溯意图检测关键词
10
+ * 分为中文和英文两组,按置信度排序
11
+ */
12
+ const ROLLBACK_PATTERNS = [
13
+ // 高置信度 — 明确的回退指令(中文不用 \b,英文保留)
14
+ { pattern: /(回退|撤销|撤回|回滚|还原)/i, confidence: 0.9 },
15
+ { pattern: /\b(undo|rollback|revert)\b/i, confidence: 0.9 },
16
+ // 中置信度 — 需要上下文
17
+ { pattern: /(恢复到|恢复之前|回到之前|退回|取消(刚才|上次|之前)的(修改|更改|变更|操作))/i, confidence: 0.8 },
18
+ { pattern: /\b(restore previous|go back|undo (last|previous|recent))\b/i, confidence: 0.8 },
19
+ // 低置信度 — 可能是回退也可能不是
20
+ { pattern: /(不要了|算了不改了|改回去|恢复原样)/i, confidence: 0.7 }
21
+ ]
22
+
23
+ /**
24
+ * 检测用户消息中的回溯意图
25
+ * @param {string} text - 用户输入文本
26
+ * @returns {{ isRollback: boolean, confidence: number, matchedPattern: string }}
27
+ */
28
+ export function detectRollbackIntent(text) {
29
+ if (!text || typeof text !== "string") {
30
+ return { isRollback: false, confidence: 0, matchedPattern: "" }
31
+ }
32
+
33
+ const normalized = text.trim().toLowerCase()
34
+ // 过短的消息不太可能是回退指令(除非就是 "undo" 这样的单词)
35
+ if (normalized.length > 200) {
36
+ return { isRollback: false, confidence: 0, matchedPattern: "" }
37
+ }
38
+
39
+ for (const { pattern, confidence } of ROLLBACK_PATTERNS) {
40
+ const match = normalized.match(pattern)
41
+ if (match) {
42
+ return { isRollback: true, confidence, matchedPattern: match[0] }
43
+ }
44
+ }
45
+
46
+ return { isRollback: false, confidence: 0, matchedPattern: "" }
47
+ }
48
+
49
+ /**
50
+ * 向用户确认是否执行回滚,并展示可用快照
51
+ * @returns {{ confirmed: boolean, snapshotId: string|null, message: string }}
52
+ */
53
+ export async function confirmRollback({ cwd, language = "en" }) {
54
+ const inGit = await isGitRepo(cwd)
55
+ if (!inGit) {
56
+ return {
57
+ confirmed: false,
58
+ snapshotId: null,
59
+ message: language === "zh"
60
+ ? "当前目录不是 Git 仓库,无法执行代码回滚。"
61
+ : "Not a git repository — cannot rollback code changes."
62
+ }
63
+ }
64
+
65
+ const latest = await getLatestGhostCommit(cwd)
66
+ if (!latest) {
67
+ return {
68
+ confirmed: false,
69
+ snapshotId: null,
70
+ message: language === "zh"
71
+ ? "没有找到可用的快照。本次会话尚未创建任何代码快照,无法回滚。"
72
+ : "No snapshots found. No code snapshots were created in this session."
73
+ }
74
+ }
75
+
76
+ const snapDate = new Date(latest.createdAt).toLocaleString()
77
+ const fileCount = latest.files?.length || 0
78
+ const shortHash = latest.commitHash?.slice(0, 8) || "unknown"
79
+
80
+ const zhWarning = [
81
+ `找到最近的快照: ${shortHash} (${snapDate})`,
82
+ `包含 ${fileCount} 个文件: ${(latest.files || []).slice(0, 5).join(", ")}${fileCount > 5 ? " ..." : ""}`,
83
+ "",
84
+ "⚠ 注意: 回滚只能恢复文件变更。已执行的 bash 命令(如安装依赖、删除文件等)无法自动撤销。"
85
+ ].join("\n")
86
+
87
+ const enWarning = [
88
+ `Latest snapshot: ${shortHash} (${snapDate})`,
89
+ `Contains ${fileCount} file(s): ${(latest.files || []).slice(0, 5).join(", ")}${fileCount > 5 ? " ..." : ""}`,
90
+ "",
91
+ "Warning: Rollback only restores file changes. Bash commands (installs, deletions, etc.) cannot be undone."
92
+ ].join("\n")
93
+
94
+ const answers = await askQuestionInteractive({
95
+ questions: [{
96
+ id: "rollback_confirm",
97
+ text: language === "zh" ? "确认回滚代码?" : "Confirm code rollback?",
98
+ description: language === "zh" ? zhWarning : enWarning,
99
+ options: [
100
+ {
101
+ label: language === "zh" ? "确认回滚" : "Confirm rollback",
102
+ value: "yes",
103
+ description: language === "zh"
104
+ ? "恢复文件到快照状态"
105
+ : "Restore files to snapshot state"
106
+ },
107
+ {
108
+ label: language === "zh" ? "取消" : "Cancel",
109
+ value: "no",
110
+ description: language === "zh"
111
+ ? "不执行回滚,继续当前对话"
112
+ : "Skip rollback, continue conversation"
113
+ }
114
+ ],
115
+ allowCustom: false
116
+ }]
117
+ })
118
+
119
+ const answer = String(answers.rollback_confirm || "").toLowerCase().trim()
120
+ const confirmed = ["yes", "confirm", "确认回滚", "1"].includes(answer)
121
+
122
+ return {
123
+ confirmed,
124
+ snapshotId: confirmed ? latest.id : null,
125
+ commitHash: confirmed ? latest.commitHash : null,
126
+ message: confirmed
127
+ ? (language === "zh" ? `正在回滚到快照 ${shortHash}...` : `Rolling back to snapshot ${shortHash}...`)
128
+ : (language === "zh" ? "已取消回滚。" : "Rollback cancelled.")
129
+ }
130
+ }
131
+
132
+ /**
133
+ * 执行代码回滚
134
+ * @returns {{ ok: boolean, message: string }}
135
+ */
136
+ export async function executeRollback({ cwd, commitHash, sessionId, language = "en" }) {
137
+ try {
138
+ const result = await restoreGhostCommit(cwd, commitHash, false)
139
+ if (!result.ok) {
140
+ return {
141
+ ok: false,
142
+ message: language === "zh"
143
+ ? `回滚失败: ${result.error}`
144
+ : `Rollback failed: ${result.error}`
145
+ }
146
+ }
147
+
148
+ await EventBus.emit({
149
+ type: EVENT_TYPES.TURN_STEP_FINISH,
150
+ sessionId,
151
+ payload: { action: "rollback", commitHash }
152
+ })
153
+
154
+ return {
155
+ ok: true,
156
+ message: language === "zh"
157
+ ? `已成功回滚到快照 ${commitHash.slice(0, 8)}。文件已恢复,但已执行的 bash 命令无法撤销。`
158
+ : `Rolled back to snapshot ${commitHash.slice(0, 8)}. Files restored, but executed bash commands cannot be undone.`
159
+ }
160
+ } catch (err) {
161
+ return {
162
+ ok: false,
163
+ message: language === "zh"
164
+ ? `回滚异常: ${err.message}`
165
+ : `Rollback error: ${err.message}`
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * 完整的回溯流程:检测 → 确认 → 执行
172
+ * 在 loop.mjs 的 processTurnLoop 入口调用
173
+ *
174
+ * @returns {{ handled: boolean, reply: string }}
175
+ * handled=true 表示消息已被回溯流程处理,不需要再发给模型
176
+ */
177
+ export async function handleRollbackIfNeeded({ prompt, cwd, sessionId, language = "en" }) {
178
+ const intent = detectRollbackIntent(prompt)
179
+ if (!intent.isRollback) {
180
+ return { handled: false, reply: "" }
181
+ }
182
+
183
+ const confirmation = await confirmRollback({ cwd, language })
184
+ if (!confirmation.confirmed) {
185
+ return { handled: true, reply: confirmation.message }
186
+ }
187
+
188
+ const result = await executeRollback({
189
+ cwd,
190
+ commitHash: confirmation.commitHash,
191
+ sessionId,
192
+ language
193
+ })
194
+
195
+ return { handled: true, reply: result.message }
196
+ }
@@ -222,7 +222,7 @@ export async function touchSession({
222
222
  model,
223
223
  providerType,
224
224
  cwd,
225
- title: title || existing?.title || `${mode}:${model}`,
225
+ title: existing?.title || title || `${mode}:${model}`,
226
226
  status,
227
227
  parentSessionId: parentSessionId || existing?.parentSessionId || null,
228
228
  forkFrom: forkFrom || existing?.forkFrom || null,
@@ -326,9 +326,18 @@ export async function getConversationHistory(sessionId, limit = 30) {
326
326
  return withLock(async () => {
327
327
  await ensureLoadedUnsafe()
328
328
  const data = await loadSessionDataUnsafe(sessionId)
329
- return data.messages.slice(-limit).map((msg) => ({
329
+ const msgs = data.messages
330
+ // Always preserve compaction summary (first message) — it must never be sliced off
331
+ // by the limit window, otherwise the model loses all prior context
332
+ const firstIsCompaction = msgs.length > 0 &&
333
+ typeof msgs[0].content === "string" &&
334
+ msgs[0].content.includes("<compaction-summary>")
335
+ const sliced = firstIsCompaction
336
+ ? [msgs[0], ...msgs.slice(1).slice(-limit)]
337
+ : msgs.slice(-limit)
338
+ return sliced.map((msg) => ({
330
339
  role: msg.role,
331
- content: msg.content // preserves array content blocks (images) as-is
340
+ content: msg.content
332
341
  }))
333
342
  })
334
343
  }