minimal-agent 0.18.13 → 0.18.14

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.
Files changed (174) hide show
  1. package/package.json +1 -3
  2. package/plugins/workflow-runner/src/runtime/context.js +1 -1
  3. package/plugins/workflow-runner/src/runtime/context.ts +1 -1
  4. package/plugins/workflow-runner/test/embeddedExecution.test.ts +2 -5
  5. package/plugins/workflow-runner/test/execLifetime.test.ts +15 -2
  6. package/skills/ssh-remote-dev/SKILL.md +61 -0
  7. package/skills/ssh-remote-dev/agents/openai.yaml +4 -0
  8. package/src/advisor/README.md +62 -24
  9. package/src/advisor/advisorNudge.js +199 -0
  10. package/src/advisor/advisorNudge.ts +232 -0
  11. package/src/advisor/deliveryReview.js +35 -12
  12. package/src/advisor/deliveryReview.ts +35 -11
  13. package/src/advisor/presentation.js +7 -10
  14. package/src/advisor/presentation.ts +10 -13
  15. package/src/advisor/reviewContext.js +22 -58
  16. package/src/advisor/reviewContext.ts +24 -67
  17. package/src/cli/configMenu.js +17 -128
  18. package/src/cli/configMenu.tsx +17 -193
  19. package/src/cli/print.js +2 -0
  20. package/src/cli/print.ts +2 -0
  21. package/src/cli/streamJson.js +2 -0
  22. package/src/cli/streamJson.ts +2 -0
  23. package/src/cli/wizards/advisorWizard.js +9 -7
  24. package/src/cli/wizards/advisorWizard.tsx +9 -7
  25. package/src/cli/wizards/contextAdvisorWizard.js +9 -7
  26. package/src/cli/wizards/contextAdvisorWizard.tsx +9 -7
  27. package/src/cli/wizards/providerPresets.js +27 -1
  28. package/src/cli/wizards/providerPresets.ts +44 -2
  29. package/src/cli/wizards/providerWizard.js +51 -10
  30. package/src/cli/wizards/providerWizard.tsx +59 -9
  31. package/src/config/configDefaults.js +1 -1
  32. package/src/config/configDefaults.ts +1 -1
  33. package/src/config/configDiagnostics.js +1 -21
  34. package/src/config/configDiagnostics.ts +3 -23
  35. package/src/config/configFile.js +11 -4
  36. package/src/config/configFile.ts +27 -7
  37. package/src/config/configManagement.js +1 -0
  38. package/src/config/configManagement.ts +2 -0
  39. package/src/config/configService.js +12 -8
  40. package/src/config/configService.ts +16 -11
  41. package/src/config/numericLimits.js +0 -4
  42. package/src/config/numericLimits.ts +0 -4
  43. package/src/config/reasoningEffort.js +16 -0
  44. package/src/config/reasoningEffort.ts +18 -0
  45. package/src/context/budgetPlan.js +1 -1
  46. package/src/context/budgetPlan.ts +1 -1
  47. package/src/context/optimizeConversation.js +1 -1
  48. package/src/context/optimizeConversation.ts +1 -1
  49. package/src/harness/stallGuard.js +8 -13
  50. package/src/harness/stallGuard.ts +8 -12
  51. package/src/llm/client.js +64 -17
  52. package/src/llm/client.ts +62 -7
  53. package/src/llm/providers/openaiFormat.js +14 -3
  54. package/src/llm/providers/openaiFormat.ts +17 -3
  55. package/src/llm/providers/responsesFormat.js +275 -0
  56. package/src/llm/providers/responsesFormat.ts +306 -0
  57. package/src/loop/README.md +18 -3
  58. package/src/loop/liveDiagramGate.js +63 -0
  59. package/src/loop/liveDiagramGate.ts +61 -0
  60. package/src/loop/messages.js +12 -0
  61. package/src/loop/messages.ts +13 -0
  62. package/src/loop/options.ts +7 -2
  63. package/src/loop/toolBatch.js +0 -1
  64. package/src/loop/toolBatch.ts +0 -1
  65. package/src/loop/toolExecutor.js +11 -46
  66. package/src/loop/toolExecutor.ts +12 -55
  67. package/src/loop/toolPresentation.js +2 -13
  68. package/src/loop/toolPresentation.ts +2 -13
  69. package/src/loop/toolSemantics.js +3 -4
  70. package/src/loop/toolSemantics.ts +4 -6
  71. package/src/loop.js +222 -214
  72. package/src/loop.ts +226 -232
  73. package/src/memory/dependencies.js +11 -3
  74. package/src/memory/dependencies.ts +11 -3
  75. package/src/memory/episodicMemory.js +59 -18
  76. package/src/memory/episodicMemory.ts +58 -18
  77. package/src/memory/experienceStore.js +4 -3
  78. package/src/memory/experienceStore.ts +4 -3
  79. package/src/memory/factCandidates.js +53 -1
  80. package/src/memory/factCandidates.ts +54 -1
  81. package/src/memory/factLearning.js +2 -2
  82. package/src/memory/factLearning.ts +2 -2
  83. package/src/memory/jobPolicy.js +24 -5
  84. package/src/memory/jobPolicy.ts +25 -5
  85. package/src/memory/learning.js +2 -2
  86. package/src/memory/learning.ts +2 -2
  87. package/src/memory/learningModel.js +7 -0
  88. package/src/memory/learningModel.ts +7 -0
  89. package/src/memory/managementService.js +8 -7
  90. package/src/memory/managementService.ts +16 -8
  91. package/src/memory/memoryService.js +104 -33
  92. package/src/memory/memoryService.ts +132 -28
  93. package/src/memory/projectKnowledge.js +39 -3
  94. package/src/memory/projectKnowledge.ts +45 -3
  95. package/src/memory/recallQuery.js +56 -0
  96. package/src/memory/recallQuery.ts +58 -0
  97. package/src/memory/retrieval.js +39 -10
  98. package/src/memory/retrieval.ts +46 -11
  99. package/src/memory/turnJournal.js +75 -10
  100. package/src/memory/turnJournal.ts +66 -10
  101. package/src/memory/types.ts +7 -1
  102. package/src/prompts/delegation.js +2 -1
  103. package/src/prompts/delegation.ts +3 -1
  104. package/src/prompts/policyRegistry.js +9 -12
  105. package/src/prompts/policyRegistry.ts +9 -12
  106. package/src/prompts/promptAssembly.js +6 -5
  107. package/src/prompts/promptAssembly.ts +8 -6
  108. package/src/prompts/visualPresentation.js +39 -10
  109. package/src/prompts/visualPresentation.ts +38 -9
  110. package/src/runtime/answerPreparation.js +1 -1
  111. package/src/runtime/answerPreparation.ts +1 -1
  112. package/src/runtime/fileLock.js +19 -3
  113. package/src/runtime/fileLock.ts +19 -3
  114. package/src/runtime/invocationPolicy.js +1 -1
  115. package/src/runtime/invocationPolicy.ts +1 -1
  116. package/src/runtime/outputContract.js +0 -4
  117. package/src/runtime/outputContract.ts +0 -7
  118. package/src/session/runAgentTurn.js +3 -0
  119. package/src/session/runAgentTurn.ts +3 -0
  120. package/src/team/onlineToolContract.js +0 -1
  121. package/src/team/onlineToolContract.ts +0 -1
  122. package/src/tools/advisor/advisor.js +207 -365
  123. package/src/tools/advisor/advisor.ts +211 -383
  124. package/src/tools/agent/agentTypes.js +0 -1
  125. package/src/tools/agent/agentTypes.ts +0 -1
  126. package/src/tools/agent/coordinator.js +4 -1
  127. package/src/tools/agent/coordinator.ts +4 -1
  128. package/src/tools/bash/semantics.js +4 -5
  129. package/src/tools/bash/semantics.ts +4 -5
  130. package/src/tools/capabilities.js +2 -3
  131. package/src/tools/capabilities.ts +2 -4
  132. package/src/tools/index.js +0 -16
  133. package/src/tools/index.ts +0 -16
  134. package/src/tools/memory/maintenance.js +8 -9
  135. package/src/tools/memory/maintenance.ts +13 -11
  136. package/src/tools/memory/memory.js +5 -1
  137. package/src/tools/memory/memory.ts +5 -0
  138. package/src/tools/optimizeConversation/README.md +4 -6
  139. package/src/tools/toolPromptGuides.js +2 -9
  140. package/src/tools/toolPromptGuides.ts +2 -9
  141. package/src/types.ts +28 -105
  142. package/src/ui/App.js +2 -0
  143. package/src/ui/App.tsx +1 -0
  144. package/src/ui/MessageList.js +8 -44
  145. package/src/ui/MessageList.tsx +3 -81
  146. package/src/ui/hooks/useChat.js +2 -0
  147. package/src/ui/hooks/useChat.ts +2 -0
  148. package/src/ui/toolProgress.js +1 -1
  149. package/src/ui/toolProgress.ts +1 -1
  150. package/src/web/protocol.ts +10 -3
  151. package/src/advisor/advisorCoordinator.js +0 -734
  152. package/src/advisor/advisorCoordinator.ts +0 -825
  153. package/src/cli/wizards/sshHostWizard.js +0 -156
  154. package/src/cli/wizards/sshHostWizard.tsx +0 -214
  155. package/src/context/sshHosts.js +0 -290
  156. package/src/context/sshHosts.ts +0 -312
  157. package/src/loop/publication.js +0 -94
  158. package/src/loop/publication.ts +0 -124
  159. package/src/runtime/finalAnswer.js +0 -9
  160. package/src/runtime/finalAnswer.ts +0 -13
  161. package/src/tools/advisor/advisorDecision.js +0 -79
  162. package/src/tools/advisor/advisorDecision.ts +0 -92
  163. package/src/tools/ssh/ssh.js +0 -152
  164. package/src/tools/ssh/ssh.ts +0 -167
  165. package/src/tools/ssh/sshAvailability.js +0 -35
  166. package/src/tools/ssh/sshAvailability.ts +0 -39
  167. package/src/tools/ssh/sshClient.js +0 -177
  168. package/src/tools/ssh/sshClient.ts +0 -189
  169. package/src/tools/ssh/sshDownload.js +0 -77
  170. package/src/tools/ssh/sshDownload.ts +0 -92
  171. package/src/tools/ssh/sshHostsTool.js +0 -36
  172. package/src/tools/ssh/sshHostsTool.ts +0 -48
  173. package/src/tools/ssh/sshUpload.js +0 -72
  174. package/src/tools/ssh/sshUpload.ts +0 -85
@@ -5,6 +5,23 @@ const snapshotIndexes = new WeakMap();
5
5
  export function memoryTerms(query) {
6
6
  return searchTerms(query);
7
7
  }
8
+ /**
9
+ * Stable personal facts keep their force however old they are. A preference
10
+ * like "always answer in Chinese" does not expire by calendar: age used to
11
+ * relabel it "待复核,不能当作当前已确认事实" after the review window, and
12
+ * restating it never refreshed the clock, so a standing preference silently
13
+ * stopped binding after six months. Project/decision/topic records change with
14
+ * the work, so for them age is a real signal. Only the injected label and the
15
+ * ranking penalty use this; `isReviewDue` still feeds the user's review queue.
16
+ */
17
+ export function agingDowngrades(record) {
18
+ return record.category !== 'preference' && record.category !== 'profile';
19
+ }
20
+ /** The label and ranking view of staleness: explicit marks always count, age only where it is evidence. */
21
+ export function isUnconfirmed(record, settings, now = Date.now()) {
22
+ return record.status === 'needs_review'
23
+ || (agingDowngrades(record) && isReviewDue(record, settings, now));
24
+ }
8
25
  export function isReviewDue(record, settings, now = Date.now()) {
9
26
  const days = settings.reviewAfterDays?.[record.category] ??
10
27
  DEFAULT_REVIEW_WINDOWS[record.category];
@@ -13,14 +30,18 @@ export function isReviewDue(record, settings, now = Date.now()) {
13
30
  export function scoreMemory(record, query, settings) {
14
31
  if (record.status === 'superseded' || (record.validUntil !== undefined && record.validUntil <= Date.now()))
15
32
  return Number.NEGATIVE_INFINITY;
33
+ const stalePenalty = isUnconfirmed(record, settings) ? 8 : 0;
34
+ const { terms, phrase } = substringParts(record, query);
35
+ return terms + phrase - stalePenalty + record.lastConfirmedAt / 1e13;
36
+ }
37
+ /** Substring evidence: 10 per matching query term, 25 for the whole phrase. */
38
+ function substringParts(record, query) {
16
39
  const haystack = `${record.category} ${record.subject} ${record.topicKey} ${record.aliases.join(' ')} ${record.statement}`.normalize('NFKC').toLocaleLowerCase();
17
- const terms = memoryTerms(query);
18
- const lexical = terms.reduce((sum, term) => sum + (haystack.includes(term) ? 10 : 0), 0);
40
+ const lexical = memoryTerms(query).reduce((sum, term) => sum + (haystack.includes(term) ? 10 : 0), 0);
19
41
  const phrase = query.trim() && haystack.includes(query.trim().normalize('NFKC').toLocaleLowerCase())
20
42
  ? 25
21
43
  : 0;
22
- const stalePenalty = isReviewDue(record, settings) || record.status === 'needs_review' ? 8 : 0;
23
- return lexical + phrase - stalePenalty + record.lastConfirmedAt / 1e13;
44
+ return { terms: lexical, phrase };
24
45
  }
25
46
  export function recallFromSnapshot(snapshot, query, settings, limit) {
26
47
  const eligible = snapshot.records.filter((record) => record.status !== 'superseded' && (record.validUntil === undefined || record.validUntil > Date.now()));
@@ -53,18 +74,26 @@ export function recallFromSnapshot(snapshot, query, settings, limit) {
53
74
  }
54
75
  recordSearchStrategy('bm25', eligible.length);
55
76
  const bm25 = index.search(query);
56
- const normalized = query.trim().normalize('NFKC').toLocaleLowerCase();
77
+ // BM25 only knows whole tokens, so `postgres` never reaches a record that
78
+ // says `PostgreSQL`; the small-corpus path finds it by substring. Growing
79
+ // past the threshold must not make a findable record vanish, so substring
80
+ // hits stay candidates. A token hit keeps its original score (BM25's IDF
81
+ // is what puts a rare word above two common ones, and the flat per-term
82
+ // substring bonus would flatten it); substring evidence only scores the
83
+ // records BM25 could not see.
57
84
  return eligible
58
85
  .map((record) => {
59
- const haystack = `${record.category} ${record.subject} ${record.topicKey} ${record.aliases.join(' ')} ${record.statement}`.normalize('NFKC').toLocaleLowerCase();
60
- const phrase = normalized && haystack.includes(normalized) ? 25 : 0;
61
- const stalePenalty = isReviewDue(record, settings) || record.status === 'needs_review' ? 8 : 0;
86
+ const { terms, phrase } = substringParts(record, query);
87
+ const tokens = bm25.get(record.id) ?? 0;
88
+ const stalePenalty = isUnconfirmed(record, settings) ? 8 : 0;
62
89
  return {
63
90
  record,
64
- score: (bm25.get(record.id) ?? 0) * 10 + phrase - stalePenalty + record.lastConfirmedAt / 1e13,
91
+ hit: tokens > 0 || terms + phrase >= 2,
92
+ score: (tokens > 0 ? tokens * 10 + phrase : terms + phrase)
93
+ - stalePenalty + record.lastConfirmedAt / 1e13,
65
94
  };
66
95
  })
67
- .filter((item) => (bm25.get(item.record.id) ?? 0) > 0)
96
+ .filter((item) => item.hit)
68
97
  .sort((a, b) => b.score - a.score)
69
98
  .slice(0, Math.min(100, Math.max(1, limit)))
70
99
  .map((item) => item.record);
@@ -21,6 +21,29 @@ export function memoryTerms(query: string): string[] {
21
21
  return searchTerms(query);
22
22
  }
23
23
 
24
+ /**
25
+ * Stable personal facts keep their force however old they are. A preference
26
+ * like "always answer in Chinese" does not expire by calendar: age used to
27
+ * relabel it "待复核,不能当作当前已确认事实" after the review window, and
28
+ * restating it never refreshed the clock, so a standing preference silently
29
+ * stopped binding after six months. Project/decision/topic records change with
30
+ * the work, so for them age is a real signal. Only the injected label and the
31
+ * ranking penalty use this; `isReviewDue` still feeds the user's review queue.
32
+ */
33
+ export function agingDowngrades(record: MemoryRecordV2): boolean {
34
+ return record.category !== 'preference' && record.category !== 'profile';
35
+ }
36
+
37
+ /** The label and ranking view of staleness: explicit marks always count, age only where it is evidence. */
38
+ export function isUnconfirmed(
39
+ record: MemoryRecordV2,
40
+ settings: MemorySettings,
41
+ now = Date.now(),
42
+ ): boolean {
43
+ return record.status === 'needs_review'
44
+ || (agingDowngrades(record) && isReviewDue(record, settings, now));
45
+ }
46
+
24
47
  export function isReviewDue(
25
48
  record: MemoryRecordV2,
26
49
  settings: MemorySettings,
@@ -38,10 +61,16 @@ export function scoreMemory(
38
61
  settings: MemorySettings,
39
62
  ): number {
40
63
  if (record.status === 'superseded' || (record.validUntil !== undefined && record.validUntil <= Date.now())) return Number.NEGATIVE_INFINITY;
64
+ const stalePenalty = isUnconfirmed(record, settings) ? 8 : 0;
65
+ const { terms, phrase } = substringParts(record, query);
66
+ return terms + phrase - stalePenalty + record.lastConfirmedAt / 1e13;
67
+ }
68
+
69
+ /** Substring evidence: 10 per matching query term, 25 for the whole phrase. */
70
+ function substringParts(record: MemoryRecordV2, query: string): { terms: number; phrase: number } {
41
71
  const haystack =
42
72
  `${record.category} ${record.subject} ${record.topicKey} ${record.aliases.join(' ')} ${record.statement}`.normalize('NFKC').toLocaleLowerCase();
43
- const terms = memoryTerms(query);
44
- const lexical = terms.reduce(
73
+ const lexical = memoryTerms(query).reduce(
45
74
  (sum, term) => sum + (haystack.includes(term) ? 10 : 0),
46
75
  0,
47
76
  );
@@ -49,9 +78,7 @@ export function scoreMemory(
49
78
  query.trim() && haystack.includes(query.trim().normalize('NFKC').toLocaleLowerCase())
50
79
  ? 25
51
80
  : 0;
52
- const stalePenalty =
53
- isReviewDue(record, settings) || record.status === 'needs_review' ? 8 : 0;
54
- return lexical + phrase - stalePenalty + record.lastConfirmedAt / 1e13;
81
+ return { terms: lexical, phrase };
55
82
  }
56
83
 
57
84
  export function recallFromSnapshot(
@@ -93,18 +120,26 @@ export function recallFromSnapshot(
93
120
  }
94
121
  recordSearchStrategy('bm25', eligible.length);
95
122
  const bm25 = index.search(query);
96
- const normalized = query.trim().normalize('NFKC').toLocaleLowerCase();
123
+ // BM25 only knows whole tokens, so `postgres` never reaches a record that
124
+ // says `PostgreSQL`; the small-corpus path finds it by substring. Growing
125
+ // past the threshold must not make a findable record vanish, so substring
126
+ // hits stay candidates. A token hit keeps its original score (BM25's IDF
127
+ // is what puts a rare word above two common ones, and the flat per-term
128
+ // substring bonus would flatten it); substring evidence only scores the
129
+ // records BM25 could not see.
97
130
  return eligible
98
131
  .map((record) => {
99
- const haystack = `${record.category} ${record.subject} ${record.topicKey} ${record.aliases.join(' ')} ${record.statement}`.normalize('NFKC').toLocaleLowerCase();
100
- const phrase = normalized && haystack.includes(normalized) ? 25 : 0;
101
- const stalePenalty = isReviewDue(record, settings) || record.status === 'needs_review' ? 8 : 0;
132
+ const { terms, phrase } = substringParts(record, query);
133
+ const tokens = bm25.get(record.id) ?? 0;
134
+ const stalePenalty = isUnconfirmed(record, settings) ? 8 : 0;
102
135
  return {
103
136
  record,
104
- score: (bm25.get(record.id) ?? 0) * 10 + phrase - stalePenalty + record.lastConfirmedAt / 1e13,
137
+ hit: tokens > 0 || terms + phrase >= 2,
138
+ score: (tokens > 0 ? tokens * 10 + phrase : terms + phrase)
139
+ - stalePenalty + record.lastConfirmedAt / 1e13,
105
140
  };
106
141
  })
107
- .filter((item) => (bm25.get(item.record.id) ?? 0) > 0)
142
+ .filter((item) => item.hit)
108
143
  .sort((a, b) => b.score - a.score)
109
144
  .slice(0, Math.min(100, Math.max(1, limit)))
110
145
  .map((item) => item.record);
@@ -30,6 +30,7 @@ import { scheduleMemoryMaintenance } from './learning.js';
30
30
  import { enqueueFactTurn, factLearningStatus, scheduleFactLearning } from './factLearning.js';
31
31
  import { captureDependencies } from './dependencies.js';
32
32
  import { getWorkingDir } from '../bootstrap/workingDir.js';
33
+ import { FENCE_CLOSE, FENCE_OPEN } from '../prompts/visualPresentation.js';
33
34
  import { isReadOnlyShellCommand } from '../tools/bash/semantics.js';
34
35
  function messageText(message) {
35
36
  if (typeof message.content === 'string')
@@ -48,6 +49,31 @@ function lastAssistantMessage(messages, start) {
48
49
  }
49
50
  return undefined;
50
51
  }
52
+ /**
53
+ * The turn's answer as the user read it. When a live answer paused before a
54
+ * text-drawn diagram, the part already shown was committed as its own
55
+ * text-only progress message and the model continued in the final one; both
56
+ * halves are the answer. Only contiguous text-only assistant messages right
57
+ * before the last one join it (ephemeral notices in between are skipped); tool
58
+ * rounds and user messages end the run.
59
+ */
60
+ function turnAnswerText(messages, start) {
61
+ let index = messages.length - 1;
62
+ while (index >= start && messages[index]?.role !== 'assistant')
63
+ index -= 1;
64
+ if (index < start)
65
+ return '';
66
+ const parts = [messageText(messages[index])];
67
+ for (let cursor = index - 1; cursor >= start; cursor -= 1) {
68
+ const message = messages[cursor];
69
+ if (message.role === 'system' && message._ephemeral)
70
+ continue;
71
+ if (message.role !== 'assistant' || message.tool_calls?.length || message.presentation !== 'progress')
72
+ break;
73
+ parts.unshift(messageText(message));
74
+ }
75
+ return parts.join('');
76
+ }
51
77
  const PURE_CONTINUATION = /^(?:继续|接着(?:来|做)?|好的?|行|嗯+|ok(?:ay)?|go on|continue|next|下一步|开始吧?|谢谢|thanks?)[。.!!~\s]*$/i;
52
78
  /** Both current events and legacy previews use the same minimal evidence fields. */
53
79
  function evidenceArguments(raw, toolName) {
@@ -84,18 +110,56 @@ function evidenceArguments(raw, toolName) {
84
110
  * - 斜杠命令:是给宿主的指令,不是陈述;
85
111
  * - 纯续接词(「继续」「好的」):提示词明确要求不展开成事实。明确确认(「是的」
86
112
  * 「按这个」)不在此列——它们命中下面的旧信号,是确认前文方案的唯一入口;
87
- * - 长粘贴且无明确信号:粘进来的邮件/文档里的第一人称句子能通过逐字引用闸门,
113
+ * - 长输入且无明确信号:粘进来的邮件/文档里的第一人称句子能通过逐字引用闸门,
88
114
  * 「引述他人不算」只靠提示词挡。这条是为准确性,不是为省调用。
115
+ * 曾经整条丢弃,于是 2300 字的消息末尾那句「以后回答简洁点」永远进不了队列。
116
+ * 现在只交出**用户自己写的部分**:去掉围栏代码块和 `>` 引用行(那是贴进来的资料,
117
+ * 不是陈述);仍超长就取末尾——长消息里的诉求几乎总在最后。不新增任何模型判断,
118
+ * 逐字证据闸门不变,且交出去的文本就是证据来源,引用只能出自这一段。
89
119
  */
90
- function shouldExtractFacts(userInput, explicitSignal) {
120
+ const FACT_TEXT_MAX_CHARS = 2_000;
121
+ export function factTurnText(userInput, explicitSignal) {
91
122
  const value = userInput.trim();
92
123
  if (!value || value.startsWith('/'))
93
- return false;
124
+ return undefined;
94
125
  if (explicitSignal)
95
- return true;
126
+ return userInput;
96
127
  if (PURE_CONTINUATION.test(value))
97
- return false;
98
- return value.length <= 2_000;
128
+ return undefined;
129
+ if (value.length <= FACT_TEXT_MAX_CHARS)
130
+ return userInput;
131
+ const written = userWrittenLines(value).join('\n').trim();
132
+ if (!written)
133
+ return undefined;
134
+ if (written.length <= FACT_TEXT_MAX_CHARS)
135
+ return written;
136
+ const tail = written.slice(-FACT_TEXT_MAX_CHARS);
137
+ // Start at a line boundary so the first sentence is not cut mid-way.
138
+ const boundary = tail.indexOf('\n');
139
+ return boundary >= 0 && boundary < tail.length - 200 ? tail.slice(boundary + 1) : tail;
140
+ }
141
+ /** Lines outside fenced blocks and `>` quotes: what the user actually wrote. */
142
+ function userWrittenLines(text) {
143
+ const kept = [];
144
+ let marker;
145
+ for (const raw of text.split('\n')) {
146
+ const line = raw.replace(/\r$/, '');
147
+ if (marker) {
148
+ const close = FENCE_CLOSE.exec(line);
149
+ if (close && close[1][0] === marker[0] && close[1].length >= marker.length)
150
+ marker = undefined;
151
+ continue;
152
+ }
153
+ const open = FENCE_OPEN.exec(line);
154
+ if (open) {
155
+ marker = open[1];
156
+ continue;
157
+ }
158
+ if (/^\s*>/.test(line))
159
+ continue;
160
+ kept.push(line);
161
+ }
162
+ return kept;
99
163
  }
100
164
  /**
101
165
  * 会话原文的保留清理:节流后台执行,绝不阻塞本回合。
@@ -278,8 +342,7 @@ export async function startTurnJournal(input) {
278
342
  return;
279
343
  finished = true;
280
344
  structuredInputs.clear();
281
- const assistant = lastAssistantMessage(history, startIndex);
282
- const content = assistant ? messageText(assistant) : '';
345
+ const content = turnAnswerText(history, startIndex);
283
346
  if (content)
284
347
  await episodicMemoryService.append({
285
348
  ...target,
@@ -314,11 +377,13 @@ export async function startTurnJournal(input) {
314
377
  // "记住我喜欢简洁回答" answered with "好的,已记住" but no tool call landed
315
378
  // as claims_unverified and the fact was silently dropped — the loop broke
316
379
  // in exactly the case memory exists for.
317
- if (settings.factLearning && !userInterrupted && shouldExtractFacts(input.userInput, userSignalsChange)) {
380
+ const factText = settings.factLearning && !userInterrupted
381
+ ? factTurnText(input.userInput, userSignalsChange) : undefined;
382
+ if (factText) {
318
383
  // Separate stores, separate failures: a corrupt fact queue must not
319
384
  // also cost this turn its experience source below.
320
385
  const queued = await enqueueFactTurn(target.workspaceId, {
321
- id: turnId, sessionId: target.sessionId, text: input.userInput, createdAt,
386
+ id: turnId, sessionId: target.sessionId, text: factText, createdAt,
322
387
  handledQuotes: [...handledQuotes],
323
388
  handledTopics: [...handledTopics.values()],
324
389
  confirmationContext: priorAssistant,
@@ -37,6 +37,7 @@ import type { Message } from '../types.js';
37
37
  import { enqueueFactTurn, factLearningStatus, scheduleFactLearning } from './factLearning.js';
38
38
  import { captureDependencies } from './dependencies.js';
39
39
  import { getWorkingDir } from '../bootstrap/workingDir.js';
40
+ import { FENCE_CLOSE, FENCE_OPEN } from '../prompts/visualPresentation.js';
40
41
  import { isReadOnlyShellCommand } from '../tools/bash/semantics.js';
41
42
 
42
43
  export interface TurnJournalInput {
@@ -79,6 +80,28 @@ function lastAssistantMessage(
79
80
  return undefined;
80
81
  }
81
82
 
83
+ /**
84
+ * The turn's answer as the user read it. When a live answer paused before a
85
+ * text-drawn diagram, the part already shown was committed as its own
86
+ * text-only progress message and the model continued in the final one; both
87
+ * halves are the answer. Only contiguous text-only assistant messages right
88
+ * before the last one join it (ephemeral notices in between are skipped); tool
89
+ * rounds and user messages end the run.
90
+ */
91
+ function turnAnswerText(messages: readonly Message[], start: number): string {
92
+ let index = messages.length - 1;
93
+ while (index >= start && messages[index]?.role !== 'assistant') index -= 1;
94
+ if (index < start) return '';
95
+ const parts = [messageText(messages[index]!)];
96
+ for (let cursor = index - 1; cursor >= start; cursor -= 1) {
97
+ const message = messages[cursor]!;
98
+ if (message.role === 'system' && message._ephemeral) continue;
99
+ if (message.role !== 'assistant' || message.tool_calls?.length || message.presentation !== 'progress') break;
100
+ parts.unshift(messageText(message));
101
+ }
102
+ return parts.join('');
103
+ }
104
+
82
105
  const PURE_CONTINUATION =
83
106
  /^(?:继续|接着(?:来|做)?|好的?|行|嗯+|ok(?:ay)?|go on|continue|next|下一步|开始吧?|谢谢|thanks?)[。.!!~\s]*$/i;
84
107
 
@@ -114,15 +137,47 @@ function evidenceArguments(raw: unknown, toolName: string): Record<string, unkno
114
137
  * - 斜杠命令:是给宿主的指令,不是陈述;
115
138
  * - 纯续接词(「继续」「好的」):提示词明确要求不展开成事实。明确确认(「是的」
116
139
  * 「按这个」)不在此列——它们命中下面的旧信号,是确认前文方案的唯一入口;
117
- * - 长粘贴且无明确信号:粘进来的邮件/文档里的第一人称句子能通过逐字引用闸门,
140
+ * - 长输入且无明确信号:粘进来的邮件/文档里的第一人称句子能通过逐字引用闸门,
118
141
  * 「引述他人不算」只靠提示词挡。这条是为准确性,不是为省调用。
142
+ * 曾经整条丢弃,于是 2300 字的消息末尾那句「以后回答简洁点」永远进不了队列。
143
+ * 现在只交出**用户自己写的部分**:去掉围栏代码块和 `>` 引用行(那是贴进来的资料,
144
+ * 不是陈述);仍超长就取末尾——长消息里的诉求几乎总在最后。不新增任何模型判断,
145
+ * 逐字证据闸门不变,且交出去的文本就是证据来源,引用只能出自这一段。
119
146
  */
120
- function shouldExtractFacts(userInput: string, explicitSignal: boolean): boolean {
147
+ const FACT_TEXT_MAX_CHARS = 2_000;
148
+
149
+ export function factTurnText(userInput: string, explicitSignal: boolean): string | undefined {
121
150
  const value = userInput.trim();
122
- if (!value || value.startsWith('/')) return false;
123
- if (explicitSignal) return true;
124
- if (PURE_CONTINUATION.test(value)) return false;
125
- return value.length <= 2_000;
151
+ if (!value || value.startsWith('/')) return undefined;
152
+ if (explicitSignal) return userInput;
153
+ if (PURE_CONTINUATION.test(value)) return undefined;
154
+ if (value.length <= FACT_TEXT_MAX_CHARS) return userInput;
155
+ const written = userWrittenLines(value).join('\n').trim();
156
+ if (!written) return undefined;
157
+ if (written.length <= FACT_TEXT_MAX_CHARS) return written;
158
+ const tail = written.slice(-FACT_TEXT_MAX_CHARS);
159
+ // Start at a line boundary so the first sentence is not cut mid-way.
160
+ const boundary = tail.indexOf('\n');
161
+ return boundary >= 0 && boundary < tail.length - 200 ? tail.slice(boundary + 1) : tail;
162
+ }
163
+
164
+ /** Lines outside fenced blocks and `>` quotes: what the user actually wrote. */
165
+ function userWrittenLines(text: string): string[] {
166
+ const kept: string[] = [];
167
+ let marker: string | undefined;
168
+ for (const raw of text.split('\n')) {
169
+ const line = raw.replace(/\r$/, '');
170
+ if (marker) {
171
+ const close = FENCE_CLOSE.exec(line);
172
+ if (close && close[1]![0] === marker[0] && close[1]!.length >= marker.length) marker = undefined;
173
+ continue;
174
+ }
175
+ const open = FENCE_OPEN.exec(line);
176
+ if (open) { marker = open[1]!; continue; }
177
+ if (/^\s*>/.test(line)) continue;
178
+ kept.push(line);
179
+ }
180
+ return kept;
126
181
  }
127
182
 
128
183
  /**
@@ -284,8 +339,7 @@ export async function startTurnJournal(
284
339
  if (finished) return;
285
340
  finished = true;
286
341
  structuredInputs.clear();
287
- const assistant = lastAssistantMessage(history, startIndex);
288
- const content = assistant ? messageText(assistant) : '';
342
+ const content = turnAnswerText(history, startIndex);
289
343
  if (content) await episodicMemoryService.append({
290
344
  ...target,
291
345
  role: 'assistant',
@@ -315,11 +369,13 @@ export async function startTurnJournal(
315
369
  // "记住我喜欢简洁回答" answered with "好的,已记住" but no tool call landed
316
370
  // as claims_unverified and the fact was silently dropped — the loop broke
317
371
  // in exactly the case memory exists for.
318
- if (settings.factLearning && !userInterrupted && shouldExtractFacts(input.userInput, userSignalsChange)) {
372
+ const factText = settings.factLearning && !userInterrupted
373
+ ? factTurnText(input.userInput, userSignalsChange) : undefined;
374
+ if (factText) {
319
375
  // Separate stores, separate failures: a corrupt fact queue must not
320
376
  // also cost this turn its experience source below.
321
377
  const queued = await enqueueFactTurn(target.workspaceId, {
322
- id: turnId, sessionId: target.sessionId, text: input.userInput, createdAt,
378
+ id: turnId, sessionId: target.sessionId, text: factText, createdAt,
323
379
  handledQuotes: [...handledQuotes],
324
380
  handledTopics: [...handledTopics.values()],
325
381
  confirmationContext: priorAssistant,
@@ -172,7 +172,7 @@ export interface MemoryContextBundle {
172
172
  includedIds: string[];
173
173
  estimatedTokens: number;
174
174
  sections: Array<{
175
- id: 'persona' | 'preferences' | 'relevant' | 'projectKnowledge' | 'experiences' | 'health';
175
+ id: 'persona' | 'preferences' | 'profile' | 'relevant' | 'projectKnowledge' | 'experiences' | 'index' | 'health';
176
176
  content: string;
177
177
  includedIds: string[];
178
178
  estimatedTokens: number;
@@ -189,6 +189,12 @@ export interface ConversationEpisode {
189
189
  content: string;
190
190
  createdAt: number;
191
191
  contentHash: string;
192
+ /**
193
+ * Set when one message was stored as several consecutive episodes sharing a
194
+ * `messageId` (each ≤ the per-episode cap). `omittedChars` marks a message so
195
+ * long that its middle was dropped, so a reader knows the text is not whole.
196
+ */
197
+ part?: { index: number; total: number; omittedChars?: number };
192
198
  }
193
199
 
194
200
  export interface ConversationSearchOptions {
@@ -1,3 +1,4 @@
1
+ import { CONFIG_LIMITS } from '../config/numericLimits.js';
1
2
  /** One delegation contract shared by the main prompt, tool discovery and runtime. */
2
3
  export const RESEARCH_DELEGATION_POLICY = `# 大范围阅读与并行调研
3
4
  - 先读项目说明、目录、入口与基线,建立问题地图,不先串行读完整仓库。批量阅读涉及多个独立模块、各需多轮探索时,优先同轮派发多个 Agent(agent_type="explore");数量取决于独立工作量和宿主额度,不按文件数量机械切块。
@@ -11,5 +12,5 @@ export function delegationRuntimeGuidance(input) {
11
12
  const remaining = Math.max(0, input.maxCalls - input.callsUsed);
12
13
  if (!remaining)
13
14
  return '本轮子任务派发次数已用完:收口已有子任务,其余调查由主 Agent 使用当前工具完成,不重复尝试派发。';
14
- return `本轮可使用 Agent 进行隔离委派,剩余派发次数 ${remaining},配置并发上限 ${Math.max(1, Math.min(8, input.maxConcurrent))}(实际槽位由调度器协调)。大型批量阅读先建立问题地图,再优先并行派发独立 explore 调查;不要把所有模块留在主上下文串行阅读。少量已知文件仍直接批量读。`;
15
+ return `本轮可使用 Agent 进行隔离委派,剩余派发次数 ${remaining},配置并发上限 ${Math.max(1, Math.min(CONFIG_LIMITS.subagentMaxConcurrent.max, input.maxConcurrent))}(实际槽位由调度器协调)。大型批量阅读先建立问题地图,再优先并行派发独立 explore 调查;不要把所有模块留在主上下文串行阅读。少量已知文件仍直接批量读。`;
15
16
  }
@@ -1,3 +1,5 @@
1
+ import { CONFIG_LIMITS } from '../config/numericLimits.js';
2
+
1
3
  /** One delegation contract shared by the main prompt, tool discovery and runtime. */
2
4
  export const RESEARCH_DELEGATION_POLICY = `# 大范围阅读与并行调研
3
5
  - 先读项目说明、目录、入口与基线,建立问题地图,不先串行读完整仓库。批量阅读涉及多个独立模块、各需多轮探索时,优先同轮派发多个 Agent(agent_type="explore");数量取决于独立工作量和宿主额度,不按文件数量机械切块。
@@ -16,5 +18,5 @@ export function delegationRuntimeGuidance(input: {
16
18
  if (!input.available) return '';
17
19
  const remaining = Math.max(0, input.maxCalls - input.callsUsed);
18
20
  if (!remaining) return '本轮子任务派发次数已用完:收口已有子任务,其余调查由主 Agent 使用当前工具完成,不重复尝试派发。';
19
- return `本轮可使用 Agent 进行隔离委派,剩余派发次数 ${remaining},配置并发上限 ${Math.max(1, Math.min(8, input.maxConcurrent))}(实际槽位由调度器协调)。大型批量阅读先建立问题地图,再优先并行派发独立 explore 调查;不要把所有模块留在主上下文串行阅读。少量已知文件仍直接批量读。`;
21
+ return `本轮可使用 Agent 进行隔离委派,剩余派发次数 ${remaining},配置并发上限 ${Math.max(1, Math.min(CONFIG_LIMITS.subagentMaxConcurrent.max, input.maxConcurrent))}(实际槽位由调度器协调)。大型批量阅读先建立问题地图,再优先并行派发独立 explore 调查;不要把所有模块留在主上下文串行阅读。少量已知文件仍直接批量读。`;
20
22
  }
@@ -118,7 +118,7 @@ const ROUTABLE_TOOLS = [
118
118
  'EnterWorktree', 'ExitWorktree', 'TerminalCapture', 'Snip', 'OptimizeConversation',
119
119
  ];
120
120
  const PARAMETERIZED_TOOLS = [
121
- ...ROUTABLE_TOOLS, ...MEMORY_TOOLS, 'AskUserQuestion', 'Advisor', 'AdvisorDecision', 'Agent',
121
+ ...ROUTABLE_TOOLS, ...MEMORY_TOOLS, 'AskUserQuestion', 'Advisor', 'Agent',
122
122
  'PresentResult', 'GenerateImage', 'GenerateSpeech',
123
123
  ];
124
124
  const CAPABILITY_POLICIES = [
@@ -199,7 +199,7 @@ const CAPABILITY_POLICIES = [
199
199
  render: ({ surface, output, toolNames }) => [
200
200
  visualPresentation(output ?? surface),
201
201
  toolNames.has('Skill') || toolNames.has('Read')
202
- ? '- 配图前加载可用的 dynamic-ui。'
202
+ ? '- 需要配图时从技能目录加载匹配的图形技能。'
203
203
  : '',
204
204
  ].filter(Boolean).join('\n'),
205
205
  },
@@ -249,14 +249,11 @@ const CAPABILITY_POLICIES = [
249
249
  {
250
250
  id: 'capability.advisor', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
251
251
  requiresTools: ['Advisor'],
252
- // 注意:文本内不得硬编码其他工具名(如 PresentResult),否则该工具缺席时这里
253
- // 成为唯一残留的「幽灵引用」——capability 规则交叉引用必须在 toolNames 条件下拼接。
254
- render: ({ toolNames }) => `- Advisor(顾问模型)使用规则:提供第二意见,由主 Agent 核实取舍;不授予权限、不替用户验收。
255
- 咨询时机:实质计划执行前(plan_review)、实质交付前(delivery_review)、同一问题两次或以上处理失败后(stalled)、用户明显不满意(user_dissatisfaction)、非常不确定或无法解决(uncertainty)、关键证据冲突无法消解(evidence_conflict)。换工具不清零失败次数,无关失败不合并。
256
- 文字调研、计算、比较和推荐也是交付;自信、没文件不豁免。自动咨询不再问用户;寒暄、进度、拼写修正无需咨询。
257
- 同阶段不重复问;成果、假设实质变化或新冲突才重开。questions/note 指明疑点与证据 ID,历史由宿主附带。缺材料先补查,不算验证通过。读完意见再操作;暂缓操作未执行,提醒不算咨询。
258
- 不设咨询配额,每次咨询只尝试一次;失败、空答或超时自行降级继续,不转成人工审批,也不宣称获得验证。
259
- 以代码、测试和用户要求核实意见,${toolNames.has('AdvisorDecision') ? '可用 AdvisorDecision 记录 accept/partial/reject;登记失败不阻止执行。' : '直接说明采纳判断。'}顾问不是事实来源,冲突以实测为准。`,
252
+ render: () => `- Advisor(顾问模型)使用规则:独立的第二意见,由主 Agent 核实取舍;不授予权限、不替用户验收。它能看到整段对话(含你的思考和全部工具结果),无需复述;没有工具,不能替你验证。
253
+ 咨询时机:实质工作之前(先调查清楚,再在写、改、定方案之前问);认为完成时(实质交付或下结论之前);同一问题两次或以上处理失败后;想改变既定方案或方向前;非常不确定、关键证据冲突无法消解、用户明显不满意时。换工具不清零失败次数,无关失败不合并。
254
+ 文字调研、计算、比较和推荐也是交付,自信、没文件不豁免;简短即时问答、寒暄、进度、拼写修正无需咨询;自动咨询不问用户。
255
+ 宿主只会在动态上下文里提醒你还没问,不拦截工具也不替你调用;是否咨询由你判断。每次咨询只尝试一次,不设配额;失败、空答或超时即自行继续,不转成人工审批,也不宣称获得验证。note 只写想被挑战的疑点或关键原文;缺材料先补查,不算验证通过。读完意见再操作。
256
+ 以代码、测试和用户要求核实意见:一手证据或用户明确要求否定才拒绝;自测通过不能证明意见错误,它可能没测到所指问题。已有证据与意见方向不同又无法判定时,不要默默改道,写明冲突再咨询一次。顾问不是事实来源。`,
260
257
  },
261
258
  {
262
259
  id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
@@ -271,7 +268,7 @@ const CAPABILITY_POLICIES = [
271
268
  {
272
269
  id: 'capability.agent-write-plan', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
273
270
  requiresTools: ['AgentResolve'],
274
- render: ({ toolNames }) => `- 并行写任务先由主 Agent 铺路:确认需求和验收,检查当前改动,统一共享接口、文件归属、依赖与基线。${toolNames.has('Advisor') ? '复杂/高风险设计按顾问规则咨询 Advisor 并记录采纳判断;不可用或仅手动模式时自行核实,不等待顾问。' : '复杂设计由主 Agent 核实证据和取舍,不等待未提供的顾问能力。'}小型独立修改不强制走大流程。先完成前置接口再派发依赖实现,每份任务书携带 context.baseline/acceptance/interfaces/verification、决定及证据来源和 write_scopes。共享目录仅并行不重叠的局部编辑,构建与跨文件脚本由主 Agent 在子任务退出后统一执行;需独立构建用 worktree,但它不含未提交的铺路代码,不能为方便擅自提交用户改动。共享接口、锁文件、迁移与最终集成归主 Agent 单一负责。收到求助先决定接口或范围,必要时停止旧任务重新派发;不要让多个子 Agent 自行协调扩大范围。子任务结果是候选改动:检查差异、集成到目标工作区、处理冲突并验证组合行为后,才 AgentResolve accept/complete,写任务必须填写 writeReview 及主 Agent 的真实检查证据。接管 worktree 任务先检查其分支、未提交改动和后台进程,不把失败现场当主工作区已修改。最终只汇总设计决定、实际集成、验证证据、风险与未完成项,不拼接完整子历史。`,
271
+ render: ({ toolNames }) => `- 并行写任务先由主 Agent 铺路:确认需求和验收,检查当前改动,统一共享接口、文件归属、依赖与基线。${toolNames.has('Advisor') ? '复杂/高风险设计按顾问规则咨询 Advisor;不可用或仅手动模式时自行核实,不等待顾问。' : '复杂设计由主 Agent 核实证据和取舍,不等待未提供的顾问能力。'}小型独立修改不强制走大流程。先完成前置接口再派发依赖实现,每份任务书携带 context.baseline/acceptance/interfaces/verification、决定及证据来源和 write_scopes。共享目录仅并行不重叠的局部编辑,构建与跨文件脚本由主 Agent 在子任务退出后统一执行;需独立构建用 worktree,但它不含未提交的铺路代码,不能为方便擅自提交用户改动。共享接口、锁文件、迁移与最终集成归主 Agent 单一负责。收到求助先决定接口或范围,必要时停止旧任务重新派发;不要让多个子 Agent 自行协调扩大范围。子任务结果是候选改动:检查差异、集成到目标工作区、处理冲突并验证组合行为后,才 AgentResolve accept/complete,写任务必须填写 writeReview 及主 Agent 的真实检查证据。接管 worktree 任务先检查其分支、未提交改动和后台进程,不把失败现场当主工作区已修改。最终只汇总设计决定、实际集成、验证证据、风险与未完成项,不拼接完整子历史。`,
275
272
  },
276
273
  {
277
274
  id: 'capability.delivery', layer: 'capability', profiles: MAIN_PROFILE, source: '05',
@@ -308,7 +305,7 @@ const CAPABILITY_POLICIES = [
308
305
  if (toolNames.has('SearchConversations'))
309
306
  rules.push('提及共享历史时用 SearchConversations 按内容关键词定位,片段不足才 read 和展开邻接消息;证据充分即答。无命中可换关键词,不能声称从未讨论;原文缺失须说明。');
310
307
  if (toolNames.has('MemoryMaintenance')) {
311
- rules.push('项目经验须核对环境、失效条件和依赖;召回不等于采用,不为记忆重复任务。具体维护动作以 MemoryMaintenance 工具说明为准。', '有写权限且证据充分才用 resolve/resolveKnowledge 处理候选;后台失败不等于已保存,遗忘条目不等于删除原文。');
308
+ rules.push('项目经验须核对环境、失效条件和依赖;召回不等于采用,不为记忆重复任务。具体维护动作以 MemoryMaintenance 工具说明为准。', '事实候选经用户确认后 resolve 结清;UpdateMemory 改写记录即失效。后台失败不等于已保存,遗忘条目不等于删除原文。');
312
309
  }
313
310
  return rules.map((rule) => `- ${rule}`).join('\n');
314
311
  },
@@ -170,7 +170,7 @@ const ROUTABLE_TOOLS = [
170
170
  ];
171
171
 
172
172
  const PARAMETERIZED_TOOLS = [
173
- ...ROUTABLE_TOOLS, ...MEMORY_TOOLS, 'AskUserQuestion', 'Advisor', 'AdvisorDecision', 'Agent',
173
+ ...ROUTABLE_TOOLS, ...MEMORY_TOOLS, 'AskUserQuestion', 'Advisor', 'Agent',
174
174
  'PresentResult', 'GenerateImage', 'GenerateSpeech',
175
175
  ];
176
176
 
@@ -248,7 +248,7 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
248
248
  render: ({ surface, output, toolNames }) => [
249
249
  visualPresentation(output ?? surface),
250
250
  toolNames.has('Skill') || toolNames.has('Read')
251
- ? '- 配图前加载可用的 dynamic-ui。'
251
+ ? '- 需要配图时从技能目录加载匹配的图形技能。'
252
252
  : '',
253
253
  ].filter(Boolean).join('\n'),
254
254
  },
@@ -298,14 +298,11 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
298
298
  {
299
299
  id: 'capability.advisor', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
300
300
  requiresTools: ['Advisor'],
301
- // 注意:文本内不得硬编码其他工具名(如 PresentResult),否则该工具缺席时这里
302
- // 成为唯一残留的「幽灵引用」——capability 规则交叉引用必须在 toolNames 条件下拼接。
303
- render: ({ toolNames }) => `- Advisor(顾问模型)使用规则:提供第二意见,由主 Agent 核实取舍;不授予权限、不替用户验收。
304
- 咨询时机:实质计划执行前(plan_review)、实质交付前(delivery_review)、同一问题两次或以上处理失败后(stalled)、用户明显不满意(user_dissatisfaction)、非常不确定或无法解决(uncertainty)、关键证据冲突无法消解(evidence_conflict)。换工具不清零失败次数,无关失败不合并。
305
- 文字调研、计算、比较和推荐也是交付;自信、没文件不豁免。自动咨询不再问用户;寒暄、进度、拼写修正无需咨询。
306
- 同阶段不重复问;成果、假设实质变化或新冲突才重开。questions/note 指明疑点与证据 ID,历史由宿主附带。缺材料先补查,不算验证通过。读完意见再操作;暂缓操作未执行,提醒不算咨询。
307
- 不设咨询配额,每次咨询只尝试一次;失败、空答或超时自行降级继续,不转成人工审批,也不宣称获得验证。
308
- 以代码、测试和用户要求核实意见,${toolNames.has('AdvisorDecision') ? '可用 AdvisorDecision 记录 accept/partial/reject;登记失败不阻止执行。' : '直接说明采纳判断。'}顾问不是事实来源,冲突以实测为准。`,
301
+ render: () => `- Advisor(顾问模型)使用规则:独立的第二意见,由主 Agent 核实取舍;不授予权限、不替用户验收。它能看到整段对话(含你的思考和全部工具结果),无需复述;没有工具,不能替你验证。
302
+ 咨询时机:实质工作之前(先调查清楚,再在写、改、定方案之前问);认为完成时(实质交付或下结论之前);同一问题两次或以上处理失败后;想改变既定方案或方向前;非常不确定、关键证据冲突无法消解、用户明显不满意时。换工具不清零失败次数,无关失败不合并。
303
+ 文字调研、计算、比较和推荐也是交付,自信、没文件不豁免;简短即时问答、寒暄、进度、拼写修正无需咨询;自动咨询不问用户。
304
+ 宿主只会在动态上下文里提醒你还没问,不拦截工具也不替你调用;是否咨询由你判断。每次咨询只尝试一次,不设配额;失败、空答或超时即自行继续,不转成人工审批,也不宣称获得验证。note 只写想被挑战的疑点或关键原文;缺材料先补查,不算验证通过。读完意见再操作。
305
+ 以代码、测试和用户要求核实意见:一手证据或用户明确要求否定才拒绝;自测通过不能证明意见错误,它可能没测到所指问题。已有证据与意见方向不同又无法判定时,不要默默改道,写明冲突再咨询一次。顾问不是事实来源。`,
309
306
  },
310
307
  {
311
308
  id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
@@ -320,7 +317,7 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
320
317
  {
321
318
  id: 'capability.agent-write-plan', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
322
319
  requiresTools: ['AgentResolve'],
323
- render: ({ toolNames }) => `- 并行写任务先由主 Agent 铺路:确认需求和验收,检查当前改动,统一共享接口、文件归属、依赖与基线。${toolNames.has('Advisor') ? '复杂/高风险设计按顾问规则咨询 Advisor 并记录采纳判断;不可用或仅手动模式时自行核实,不等待顾问。' : '复杂设计由主 Agent 核实证据和取舍,不等待未提供的顾问能力。'}小型独立修改不强制走大流程。先完成前置接口再派发依赖实现,每份任务书携带 context.baseline/acceptance/interfaces/verification、决定及证据来源和 write_scopes。共享目录仅并行不重叠的局部编辑,构建与跨文件脚本由主 Agent 在子任务退出后统一执行;需独立构建用 worktree,但它不含未提交的铺路代码,不能为方便擅自提交用户改动。共享接口、锁文件、迁移与最终集成归主 Agent 单一负责。收到求助先决定接口或范围,必要时停止旧任务重新派发;不要让多个子 Agent 自行协调扩大范围。子任务结果是候选改动:检查差异、集成到目标工作区、处理冲突并验证组合行为后,才 AgentResolve accept/complete,写任务必须填写 writeReview 及主 Agent 的真实检查证据。接管 worktree 任务先检查其分支、未提交改动和后台进程,不把失败现场当主工作区已修改。最终只汇总设计决定、实际集成、验证证据、风险与未完成项,不拼接完整子历史。`,
320
+ render: ({ toolNames }) => `- 并行写任务先由主 Agent 铺路:确认需求和验收,检查当前改动,统一共享接口、文件归属、依赖与基线。${toolNames.has('Advisor') ? '复杂/高风险设计按顾问规则咨询 Advisor;不可用或仅手动模式时自行核实,不等待顾问。' : '复杂设计由主 Agent 核实证据和取舍,不等待未提供的顾问能力。'}小型独立修改不强制走大流程。先完成前置接口再派发依赖实现,每份任务书携带 context.baseline/acceptance/interfaces/verification、决定及证据来源和 write_scopes。共享目录仅并行不重叠的局部编辑,构建与跨文件脚本由主 Agent 在子任务退出后统一执行;需独立构建用 worktree,但它不含未提交的铺路代码,不能为方便擅自提交用户改动。共享接口、锁文件、迁移与最终集成归主 Agent 单一负责。收到求助先决定接口或范围,必要时停止旧任务重新派发;不要让多个子 Agent 自行协调扩大范围。子任务结果是候选改动:检查差异、集成到目标工作区、处理冲突并验证组合行为后,才 AgentResolve accept/complete,写任务必须填写 writeReview 及主 Agent 的真实检查证据。接管 worktree 任务先检查其分支、未提交改动和后台进程,不把失败现场当主工作区已修改。最终只汇总设计决定、实际集成、验证证据、风险与未完成项,不拼接完整子历史。`,
324
321
  },
325
322
  {
326
323
  id: 'capability.delivery', layer: 'capability', profiles: MAIN_PROFILE, source: '05',
@@ -351,7 +348,7 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
351
348
  if (toolNames.has('MemoryMaintenance')) {
352
349
  rules.push(
353
350
  '项目经验须核对环境、失效条件和依赖;召回不等于采用,不为记忆重复任务。具体维护动作以 MemoryMaintenance 工具说明为准。',
354
- '有写权限且证据充分才用 resolve/resolveKnowledge 处理候选;后台失败不等于已保存,遗忘条目不等于删除原文。',
351
+ '事实候选经用户确认后 resolve 结清;UpdateMemory 改写记录即失效。后台失败不等于已保存,遗忘条目不等于删除原文。',
355
352
  );
356
353
  }
357
354
  return rules.map((rule) => `- ${rule}`).join('\n');
@@ -142,11 +142,7 @@ async function buildAdvisorStatusLine() {
142
142
  if (!advisor)
143
143
  return 'Advisor 状态:未配置。';
144
144
  const mode = advisor.mode === 'auto' ? '自动' : '仅手动';
145
- const vision = advisor.supportsVision && advisor.shareSensitive
146
- ? '允许图片'
147
- : advisor.supportsVision
148
- ? '支持图片但敏感共享关闭'
149
- : '不支持图片';
145
+ const vision = advisor.supportsVision ? '支持图片' : '不支持图片';
150
146
  return `Advisor 状态:${advisor.enabled ? '已启用' : '已关闭'},${advisor.provider.name}/${advisor.provider.model},${mode},${vision},超时 ${Math.round(advisor.timeoutMs / 1000)} 秒。`;
151
147
  }
152
148
  catch {
@@ -219,6 +215,7 @@ async function buildMemoryTransient(runtime) {
219
215
  return memoryService.buildContextBundle(runtime.currentUserInput ?? '', runtime.workspaceId, {
220
216
  includePersona: runtime.inheritPersona !== false,
221
217
  budgetTokens: memoryBudgetTokens(runtime),
218
+ previousInput: runtime.previousUserInput,
222
219
  });
223
220
  }
224
221
  catch {
@@ -385,9 +382,13 @@ export async function buildTransientBundle(runtime, profile, tools, options = {}
385
382
  const priorities = {
386
383
  persona: 82,
387
384
  preferences: 78,
385
+ profile: 76,
388
386
  relevant: 70,
389
387
  projectKnowledge: 68,
390
388
  experiences: 65,
389
+ // The subject index only helps the model choose what to Recall; below
390
+ // skills (45) so a tight budget drops it before anything the model acts on.
391
+ index: 40,
391
392
  health: 10,
392
393
  };
393
394
  for (const section of memory.sections) {