@zooique/memora 0.1.0 → 0.2.0

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 (190) hide show
  1. package/dist/agent/agent.d.ts +155 -23
  2. package/dist/agent/agent.d.ts.map +1 -1
  3. package/dist/agent/agent.js +386 -124
  4. package/dist/agent/agent.js.map +1 -1
  5. package/dist/agent/assembler.d.ts +8 -2
  6. package/dist/agent/assembler.d.ts.map +1 -1
  7. package/dist/agent/assembler.js +17 -6
  8. package/dist/agent/assembler.js.map +1 -1
  9. package/dist/agent/builtinToolHandlers.d.ts +1 -1
  10. package/dist/agent/builtinToolHandlers.d.ts.map +1 -1
  11. package/dist/agent/builtinToolHandlers.js +5 -3
  12. package/dist/agent/builtinToolHandlers.js.map +1 -1
  13. package/dist/agent/builtinTools.d.ts +1 -1
  14. package/dist/agent/builtinTools.js +2 -2
  15. package/dist/agent/builtinTools.js.map +1 -1
  16. package/dist/agent/constants.d.ts +27 -6
  17. package/dist/agent/constants.d.ts.map +1 -1
  18. package/dist/agent/constants.js +27 -6
  19. package/dist/agent/constants.js.map +1 -1
  20. package/dist/agent/contextManager.d.ts +35 -9
  21. package/dist/agent/contextManager.d.ts.map +1 -1
  22. package/dist/agent/contextManager.js +138 -25
  23. package/dist/agent/contextManager.js.map +1 -1
  24. package/dist/agent/loop.d.ts +41 -11
  25. package/dist/agent/loop.d.ts.map +1 -1
  26. package/dist/agent/loop.js +143 -61
  27. package/dist/agent/loop.js.map +1 -1
  28. package/dist/agent/managers/archiveCoordinator.d.ts +100 -0
  29. package/dist/agent/managers/archiveCoordinator.d.ts.map +1 -0
  30. package/dist/agent/managers/archiveCoordinator.js +127 -0
  31. package/dist/agent/managers/archiveCoordinator.js.map +1 -0
  32. package/dist/agent/managers/autoConfigRefiner.d.ts.map +1 -1
  33. package/dist/agent/managers/autoConfigRefiner.js +1 -1
  34. package/dist/agent/managers/autoConfigRefiner.js.map +1 -1
  35. package/dist/agent/managers/configManager.d.ts +13 -2
  36. package/dist/agent/managers/configManager.d.ts.map +1 -1
  37. package/dist/agent/managers/configManager.js +13 -5
  38. package/dist/agent/managers/configManager.js.map +1 -1
  39. package/dist/agent/managers/insightExtractor.d.ts +36 -3
  40. package/dist/agent/managers/insightExtractor.d.ts.map +1 -1
  41. package/dist/agent/managers/insightExtractor.js +48 -11
  42. package/dist/agent/managers/insightExtractor.js.map +1 -1
  43. package/dist/agent/managers/memoryAdvisor.d.ts +4 -2
  44. package/dist/agent/managers/memoryAdvisor.d.ts.map +1 -1
  45. package/dist/agent/managers/memoryAdvisor.js +6 -4
  46. package/dist/agent/managers/memoryAdvisor.js.map +1 -1
  47. package/dist/agent/managers/memoryDecayScheduler.d.ts +113 -0
  48. package/dist/agent/managers/memoryDecayScheduler.d.ts.map +1 -0
  49. package/dist/agent/managers/memoryDecayScheduler.js +134 -0
  50. package/dist/agent/managers/memoryDecayScheduler.js.map +1 -0
  51. package/dist/agent/managers/memoryInspector.d.ts +70 -10
  52. package/dist/agent/managers/memoryInspector.d.ts.map +1 -1
  53. package/dist/agent/managers/memoryInspector.js +97 -23
  54. package/dist/agent/managers/memoryInspector.js.map +1 -1
  55. package/dist/agent/managers/sessionArchiver.d.ts +77 -0
  56. package/dist/agent/managers/sessionArchiver.d.ts.map +1 -0
  57. package/dist/agent/managers/sessionArchiver.js +175 -0
  58. package/dist/agent/managers/sessionArchiver.js.map +1 -0
  59. package/dist/agent/managers/sessionManager.d.ts +10 -0
  60. package/dist/agent/managers/sessionManager.d.ts.map +1 -1
  61. package/dist/agent/managers/sessionManager.js +26 -5
  62. package/dist/agent/managers/sessionManager.js.map +1 -1
  63. package/dist/agent/managers/workProjection.d.ts +1 -1
  64. package/dist/agent/managers/workProjection.d.ts.map +1 -1
  65. package/dist/agent/managers/workProjection.js +13 -10
  66. package/dist/agent/managers/workProjection.js.map +1 -1
  67. package/dist/agent/messageHistory.d.ts +1 -22
  68. package/dist/agent/messageHistory.d.ts.map +1 -1
  69. package/dist/agent/messageHistory.js +3 -19
  70. package/dist/agent/messageHistory.js.map +1 -1
  71. package/dist/agent/toolExecutor.d.ts +5 -18
  72. package/dist/agent/toolExecutor.d.ts.map +1 -1
  73. package/dist/agent/toolExecutor.js +7 -7
  74. package/dist/agent/toolExecutor.js.map +1 -1
  75. package/dist/agent/tracer.d.ts +8 -1
  76. package/dist/agent/tracer.d.ts.map +1 -1
  77. package/dist/agent/tracer.js +8 -1
  78. package/dist/agent/tracer.js.map +1 -1
  79. package/dist/agent/types.d.ts +29 -0
  80. package/dist/agent/types.d.ts.map +1 -1
  81. package/dist/agent/{managers/userFactExtractor.d.ts → userFactExtractor.d.ts} +2 -2
  82. package/dist/agent/userFactExtractor.d.ts.map +1 -0
  83. package/dist/agent/userFactExtractor.js.map +1 -0
  84. package/dist/config/loader.d.ts +4 -4
  85. package/dist/config/loader.d.ts.map +1 -1
  86. package/dist/config/loader.js +11 -2
  87. package/dist/config/loader.js.map +1 -1
  88. package/dist/eval/evalTypes.js +1 -1
  89. package/dist/eval/evalTypes.js.map +1 -1
  90. package/dist/index.d.ts +5 -2
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +2 -1
  93. package/dist/index.js.map +1 -1
  94. package/dist/llm/embedding.d.ts.map +1 -1
  95. package/dist/llm/embedding.js +8 -6
  96. package/dist/llm/embedding.js.map +1 -1
  97. package/dist/llm/factory.d.ts.map +1 -1
  98. package/dist/llm/factory.js +2 -1
  99. package/dist/llm/factory.js.map +1 -1
  100. package/dist/llm/openaiCompatible.d.ts +9 -0
  101. package/dist/llm/openaiCompatible.d.ts.map +1 -1
  102. package/dist/llm/openaiCompatible.js +77 -33
  103. package/dist/llm/openaiCompatible.js.map +1 -1
  104. package/dist/llm/provider.d.ts +17 -3
  105. package/dist/llm/provider.d.ts.map +1 -1
  106. package/dist/llm/provider.js +6 -2
  107. package/dist/llm/provider.js.map +1 -1
  108. package/dist/logging/logger.d.ts.map +1 -1
  109. package/dist/logging/logger.js +25 -9
  110. package/dist/logging/logger.js.map +1 -1
  111. package/dist/memory/hybridMerge.d.ts +54 -0
  112. package/dist/memory/hybridMerge.d.ts.map +1 -0
  113. package/dist/memory/hybridMerge.js +36 -0
  114. package/dist/memory/hybridMerge.js.map +1 -0
  115. package/dist/memory/inMemoryStorage.d.ts +90 -15
  116. package/dist/memory/inMemoryStorage.d.ts.map +1 -1
  117. package/dist/memory/inMemoryStorage.js +179 -28
  118. package/dist/memory/inMemoryStorage.js.map +1 -1
  119. package/dist/memory/loader.js +1 -1
  120. package/dist/memory/loader.js.map +1 -1
  121. package/dist/memory/projectManager.d.ts +34 -5
  122. package/dist/memory/projectManager.d.ts.map +1 -1
  123. package/dist/memory/projectManager.js +98 -62
  124. package/dist/memory/projectManager.js.map +1 -1
  125. package/dist/memory/recall.d.ts +7 -18
  126. package/dist/memory/recall.d.ts.map +1 -1
  127. package/dist/memory/recall.js +14 -19
  128. package/dist/memory/recall.js.map +1 -1
  129. package/dist/memory/storageInterface.d.ts +81 -14
  130. package/dist/memory/storageInterface.d.ts.map +1 -1
  131. package/dist/memory/store.js +1 -1
  132. package/dist/memory/store.js.map +1 -1
  133. package/dist/memory/types.d.ts +7 -1
  134. package/dist/memory/types.d.ts.map +1 -1
  135. package/dist/memory/types.js +5 -1
  136. package/dist/memory/types.js.map +1 -1
  137. package/dist/memory/userProfile.d.ts +6 -6
  138. package/dist/memory/userProfile.d.ts.map +1 -1
  139. package/dist/memory/userProfile.js +20 -15
  140. package/dist/memory/userProfile.js.map +1 -1
  141. package/dist/memory/vectorStore.d.ts +35 -0
  142. package/dist/memory/vectorStore.d.ts.map +1 -1
  143. package/dist/memory/vectorStore.js +111 -2
  144. package/dist/memory/vectorStore.js.map +1 -1
  145. package/dist/persona/personaManager.d.ts +17 -4
  146. package/dist/persona/personaManager.d.ts.map +1 -1
  147. package/dist/persona/personaManager.js +44 -12
  148. package/dist/persona/personaManager.js.map +1 -1
  149. package/dist/security/pathGuard.d.ts +29 -7
  150. package/dist/security/pathGuard.d.ts.map +1 -1
  151. package/dist/security/pathGuard.js +148 -81
  152. package/dist/security/pathGuard.js.map +1 -1
  153. package/dist/skill/skillManager.d.ts +15 -0
  154. package/dist/skill/skillManager.d.ts.map +1 -1
  155. package/dist/skill/skillManager.js +25 -3
  156. package/dist/skill/skillManager.js.map +1 -1
  157. package/dist/skill/types.d.ts +7 -1
  158. package/dist/skill/types.d.ts.map +1 -1
  159. package/dist/utils/errors.d.ts +26 -7
  160. package/dist/utils/errors.d.ts.map +1 -1
  161. package/dist/utils/errors.js +10 -18
  162. package/dist/utils/errors.js.map +1 -1
  163. package/dist/utils/eventEmitter.d.ts +9 -2
  164. package/dist/utils/eventEmitter.d.ts.map +1 -1
  165. package/dist/utils/eventEmitter.js +1 -1
  166. package/dist/utils/eventEmitter.js.map +1 -1
  167. package/dist/utils/frontmatter.d.ts +8 -1
  168. package/dist/utils/frontmatter.d.ts.map +1 -1
  169. package/dist/utils/frontmatter.js +14 -3
  170. package/dist/utils/frontmatter.js.map +1 -1
  171. package/dist/utils/path.d.ts +5 -1
  172. package/dist/utils/path.d.ts.map +1 -1
  173. package/dist/utils/path.js +5 -1
  174. package/dist/utils/path.js.map +1 -1
  175. package/dist/utils/safeTimer.d.ts +19 -0
  176. package/dist/utils/safeTimer.d.ts.map +1 -1
  177. package/dist/utils/safeTimer.js +27 -0
  178. package/dist/utils/safeTimer.js.map +1 -1
  179. package/dist/utils/segmenter.d.ts +0 -12
  180. package/dist/utils/segmenter.d.ts.map +1 -1
  181. package/dist/utils/segmenter.js +8 -3
  182. package/dist/utils/segmenter.js.map +1 -1
  183. package/dist/utils/strings.d.ts +1 -1
  184. package/dist/utils/strings.d.ts.map +1 -1
  185. package/dist/utils/strings.js +4 -2
  186. package/dist/utils/strings.js.map +1 -1
  187. package/package.json +1 -1
  188. package/dist/agent/managers/userFactExtractor.d.ts.map +0 -1
  189. package/dist/agent/managers/userFactExtractor.js.map +0 -1
  190. /package/dist/agent/{managers/userFactExtractor.js → userFactExtractor.js} +0 -0
@@ -27,20 +27,22 @@ import { AGENT_CONSTANTS } from '../agent/constants.js';
27
27
  import { ProjectManager } from '../memory/projectManager.js';
28
28
  import { SecurityGuard } from '../security/pathGuard.js';
29
29
  import { recall } from '../memory/recall.js';
30
- import { extractUserFacts } from '../agent/managers/userFactExtractor.js';
30
+ import { extractUserFacts } from '../agent/userFactExtractor.js';
31
31
  import { assembleComponents } from '../agent/assembler.js';
32
- import { configError, toError } from '../utils/errors.js';
33
- import { safeSetTimeout, clearSafeTimeout, safeSetInterval, clearSafeInterval } from '../utils/safeTimer.js';
32
+ import { configError } from '../utils/errors.js';
33
+ import { safeSetTimeout, clearSafeTimeout, clearSafeInterval } from '../utils/safeTimer.js';
34
34
  import { SessionManager } from '../agent/managers/sessionManager.js';
35
+ import { MemoryDecayScheduler } from '../agent/managers/memoryDecayScheduler.js';
36
+ import { ArchiveCoordinator } from '../agent/managers/archiveCoordinator.js';
35
37
  import { TypedEventEmitter } from '../utils/eventEmitter.js';
36
- import { SOURCE_LABELS } from '../memory/types.js';
37
38
  import { logger } from '../logging/logger.js';
38
- import { NOOP_TRACER, TRACE_SPANS } from '../agent/tracer.js';
39
39
  // ─── 模块级常量 ─────────────────────────────────────────
40
40
  /** Agent 事件名白名单,用于运行时校验 SessionManager 转发的事件类型 */
41
+ // 必须与 utils/eventEmitter.ts 的 AgentEventMap 键集保持一致(9 个事件)
41
42
  const AGENT_EVENT_NAMES = new Set([
42
43
  'memoryAdded', 'personaSwitched', 'decayCompleted',
43
44
  'memoryRecalled', 'sessionForked', 'insightExtracted',
45
+ 'conflictDetected', 'projectSwitched', 'skillMatched',
44
46
  ]);
45
47
  // ─── Agent 门面类 ───────────────────────────────────────
46
48
  /**
@@ -94,8 +96,10 @@ export class Agent extends TypedEventEmitter {
94
96
  configManager = null;
95
97
  memoryInspector = null;
96
98
  workProjection = null;
97
- /** V-201: AutoConfigRefiner(模式 3:Agent 智能总结) */
99
+ /** AutoConfigRefiner(模式 3:Agent 智能总结) */
98
100
  autoConfigRefiner = null;
101
+ /** SessionArchiver(会话内容归档器,content 类记忆) */
102
+ sessionArchiver = null;
99
103
  /** 会话管理器(从 Agent 拆分出的会话管理职责) */
100
104
  _sessionManager = null;
101
105
  /** 当前激活的技能名(上一轮匹配,本轮注入) */
@@ -105,22 +109,38 @@ export class Agent extends TypedEventEmitter {
105
109
  pctx = null;
106
110
  /** chat() 并发锁 */
107
111
  _chatBusy = false;
112
+ /**
113
+ * chat() 锁持有者 token(race condition 防护)
114
+ *
115
+ * 设计目的:
116
+ * 原锁是简单布尔值 `_chatBusy`,无 owner 校验。超时回调与 finally 块无差别清理,
117
+ * 导致 race condition:T=0 A 获取锁 → T=180s 超时释放 → T=181s B 获取锁
118
+ * → T=182s A 的 finally 误清 B 的锁/计时器/controller。
119
+ *
120
+ * 防护方案:
121
+ * - 获取锁时 token 递增:`const myToken = ++this._chatLockToken`
122
+ * - 超时回调校验 `this._chatLockToken === myToken` 后才释放
123
+ * - finally 块校验 `this._chatLockToken === myToken` 后才清理
124
+ * - 不匹配时跳过清理,避免误清新调用者的资源
125
+ *
126
+ * 选择 number 而非 Symbol:递增计数器可序列化、零依赖、足够唯一(同一 Agent 实例内)
127
+ */
128
+ _chatLockToken = 0;
108
129
  /** 聊天锁超时计时器(防止 LLM 卡死时锁永久持有) */
109
130
  chatLockTimer = null;
110
131
  /** chat() 内部 AbortController(超时时中断 generator,防止并发) */
111
132
  chatAbortController = null;
112
- /** 记忆衰减定时器 */
133
+ /** 记忆衰减定时器(已迁移至 MemoryDecayScheduler,此字段保留用于 close 时引用判断) */
113
134
  decayTimer = null;
114
135
  /** 最近一次 chat() 调用的时间戳 */
115
136
  _lastInteractionAt = null;
116
- // ─── R-103 衰减指标统计字段 ──────────────────────────
117
- // 累计值,从 Agent.init() 起累加,close() 后随实例销毁。
118
- /** 衰减执行次数(每次 runMemoryDecay 实际执行 +1) */
119
- metricDecayRunCount = 0;
120
- /** 累计衰减记忆数(score 被调低的记忆条数总和) */
121
- metricTotalDecayedCount = 0;
122
- /** 上次衰减时间(ISO 8601,null 表示从未执行过) */
123
- metricLastDecayAt = null;
137
+ // ─── 衰减职责已拆分至 MemoryDecayScheduler ──────────
138
+ // metricDecayRunCount / metricTotalDecayedCount / metricLastDecayAt 字段
139
+ // 位于 MemoryDecayScheduler 内部,Agent 通过 memoryDecayScheduler.getMetrics() 读取
140
+ /** 记忆衰减调度器(init 时创建,close 时销毁) */
141
+ memoryDecayScheduler = null;
142
+ /** 归档协调器(归档操作委托给 ArchiveCoordinator) */
143
+ archiveCoordinator = null;
124
144
  constructor(opts) {
125
145
  super();
126
146
  this.#config = {
@@ -143,6 +163,7 @@ export class Agent extends TypedEventEmitter {
143
163
  tracer: opts.tracer,
144
164
  messages: opts.messages,
145
165
  enableContextSummary: opts.enableContextSummary ?? true,
166
+ archiveMode: opts.archiveMode ?? 'full',
146
167
  };
147
168
  this.#provider = opts.provider;
148
169
  this.#backgroundProvider = opts.backgroundProvider ?? null;
@@ -171,7 +192,7 @@ export class Agent extends TypedEventEmitter {
171
192
  dataDir: this.#config.dataDir,
172
193
  storage: this.#config.storage,
173
194
  registryDir: this.#config.registryDir,
174
- // A-004: SecurityGuard 由 Agent 层创建,解除 memory→security 反向依赖
195
+ // SecurityGuard 由 Agent 层创建,解除 memory→security 反向依赖
175
196
  createSecurityGuard: (projectPath, memoraDir, configDir, agentDataDir) => new SecurityGuard(projectPath, memoraDir, this.#config.allowedPaths, this.#config.confirmWrites, this.#config.permission, configDir ?? this.#config.configDir, agentDataDir ?? this.#config.dataDir),
176
197
  });
177
198
  const pctx = await this.projectManager.initProject(this.#config.projectPath, undefined, this.#config.configDir);
@@ -185,10 +206,22 @@ export class Agent extends TypedEventEmitter {
185
206
  ]);
186
207
  }
187
208
  this._initialized = true;
188
- // 启动时记忆衰减(insight/archive 来源)
189
- this.runMemoryDecay();
190
- // 定期记忆衰减(每小时)
191
- this.decayTimer = safeSetInterval(() => this.runMemoryDecay(), AGENT_CONSTANTS.DECAY_INTERVAL_MS);
209
+ // 归档操作委托给 ArchiveCoordinator
210
+ // 使用 getter 回调注入依赖,close 时 null 化字段后 getter 自然返回 null
211
+ this.archiveCoordinator = new ArchiveCoordinator({
212
+ getUserProfile: () => this.#userProfile,
213
+ getInsightExtractor: () => this.insightExtractor,
214
+ getSessionArchiver: () => this.sessionArchiver,
215
+ emit: (event, payload) => this.emit(event, payload),
216
+ });
217
+ // 记忆衰减职责委托给 MemoryDecayScheduler
218
+ this.memoryDecayScheduler = new MemoryDecayScheduler({
219
+ tracer: this.#config.tracer,
220
+ onDecayCompleted: (payload) => this.emit('decayCompleted', payload),
221
+ });
222
+ this.memoryDecayScheduler.start(pctx.index, AGENT_CONSTANTS.DECAY_INTERVAL_MS);
223
+ // 保留 decayTimer 引用用于 close 时序兼容(实际定时器由 MemoryDecayScheduler 管理)
224
+ this.decayTimer = null;
192
225
  return pctx;
193
226
  }
194
227
  /**
@@ -206,7 +239,10 @@ export class Agent extends TypedEventEmitter {
206
239
  ]);
207
240
  }
208
241
  this._chatBusy = true;
209
- // SEC-02: 内部 AbortController,超时时中断 generator 而非仅释放锁
242
+ // 分配本调用的 token,超时回调和 finally 块据此判断是否仍是当前持有者
243
+ // 避免 race condition:超时释放后新调用获取锁,旧 finally 误清新调用者的资源
244
+ const myToken = ++this._chatLockToken;
245
+ // 内部 AbortController,超时时中断 generator 而非仅释放锁
210
246
  const internalAbort = new AbortController();
211
247
  this.chatAbortController = internalAbort;
212
248
  // 合并外部 signal:外部 abort 时也触发内部
@@ -219,7 +255,12 @@ export class Agent extends TypedEventEmitter {
219
255
  }
220
256
  const combinedSignal = internalAbort.signal;
221
257
  // 超时保护:LLM 卡死时中断 generator + 释放锁,防止并发
258
+ // 超时回调校验 token 后才清理,避免误清新调用者的资源
222
259
  this.chatLockTimer = safeSetTimeout(() => {
260
+ // 令牌不匹配:锁已被新调用者获取(或本调用已正常退出),跳过清理
261
+ if (this._chatLockToken !== myToken) {
262
+ return;
263
+ }
223
264
  logger.warn({ timeoutMs: AGENT_CONSTANTS.CHAT_LOCK_TIMEOUT_MS }, 'chat() 锁超时,中断 generator 并释放锁');
224
265
  internalAbort.abort();
225
266
  this._chatBusy = false;
@@ -247,9 +288,8 @@ export class Agent extends TypedEventEmitter {
247
288
  await this.requireNonNull(this.history, 'history').appendUser(input);
248
289
  let assistantContent = '';
249
290
  let wasAborted = false;
250
- // 修复 P1-D:原实现 generator 抛错直接传到宿主,宿主未 catch 会变未处理 rejection
251
- // (表现为"几个字就卡住、无错误日志"——流式输出已开始但错误没机会展示)
252
- // 改为 try/catch:把 provider 异常转为 yield error chunk,让宿主能优雅展示并清理 UI
291
+ // provider 异常转为 yield error chunk,让宿主能优雅展示并清理 UI
292
+ // (裸 throw 会导致未处理 rejection,UI 收不到错误展示机会)
253
293
  try {
254
294
  for await (const chunk of this.requireNonNull(this.loop, 'loop').processUserInput(input, recalledMemories, combinedSignal)) {
255
295
  yield chunk;
@@ -266,8 +306,8 @@ export class Agent extends TypedEventEmitter {
266
306
  // (如 LLM 超时、连接断开、AbortError 未被 loop 拦截等)
267
307
  // 转为 error chunk 通知宿主,避免裸 throw 导致 UI 卡死
268
308
  if (err instanceof DOMException && err.name === 'AbortError') {
269
- // 翠幕天罗 P1 修复:必须 yield aborted chunk 让宿主能展示中断标记
270
- // (原仅设置 wasAborted 会导致 chatHandlers catch 不触发,渲染层收不到 ABORTED)
309
+ // 必须 yield aborted chunk 让宿主能展示中断标记
310
+ // (仅设置 wasAborted 会导致 chatHandlers catch 不触发,渲染层收不到 ABORTED)
271
311
  yield { type: 'aborted', reason: 'User cancelled the conversation' };
272
312
  return;
273
313
  }
@@ -277,6 +317,18 @@ export class Agent extends TypedEventEmitter {
277
317
  }
278
318
  }
279
319
  if (wasAborted) {
320
+ // 流式中断时仍保留已生成的部分文本到历史,避免下一轮上下文丢失
321
+ // 追加 interrupted 标记让下一轮 LLM 和历史归档能识别这是中断响应(非完整回复)
322
+ // 与下方正常路径一致采用 best-effort 写入(失败不影响中断流程)
323
+ if (assistantContent.trim()) {
324
+ const interruptedMark = this.#config.messages?.interrupted ?? '\n\n[已中断]';
325
+ try {
326
+ await this.requireNonNull(this.history, 'history').appendAssistant(assistantContent + interruptedMark);
327
+ }
328
+ catch (err) {
329
+ logger.warn({ err }, '中断消息历史写入失败');
330
+ }
331
+ }
280
332
  return;
281
333
  }
282
334
  // 追加助手消息到历史(best-effort:失败不影响用户已收到的回答)
@@ -291,15 +343,62 @@ export class Agent extends TypedEventEmitter {
291
343
  await this.postProcess(input, assistantContent);
292
344
  }
293
345
  finally {
294
- this._chatBusy = false;
295
- this.chatAbortController = null;
296
- if (this.chatLockTimer) {
297
- clearSafeTimeout(this.chatLockTimer);
298
- this.chatLockTimer = null;
346
+ // 仅当本调用仍是当前锁持有者时才清理资源
347
+ // token 已变(超时释放后被新调用者获取),跳过清理避免误清新调用者的状态
348
+ if (this._chatLockToken === myToken) {
349
+ this._chatBusy = false;
350
+ this.chatAbortController = null;
351
+ if (this.chatLockTimer) {
352
+ clearSafeTimeout(this.chatLockTimer);
353
+ this.chatLockTimer = null;
354
+ }
299
355
  }
300
356
  signal?.removeEventListener('abort', onExternalAbort);
301
357
  }
302
358
  }
359
+ /**
360
+ * 强制释放对话锁
361
+ *
362
+ * 场景:宿主主进程"无进展超时"兜底后,generator 可能仍卡在不可中断的 await 点
363
+ * (executeToolCalls 和 generateContextSummary 已覆盖 signal 响应,但其他
364
+ * 第三方库或未来新增的 await 点仍可能不响应 signal)。
365
+ * 此时 _chatBusy 锁未释放,用户再发消息会被 chat() 的竞态保护拒绝,
366
+ * 表现为"UI 能操作但发不出消息",需等到 3 分钟锁超时才能恢复。
367
+ *
368
+ * 本方法让宿主在确认 generator 挂起后强制释放锁,让用户能立即发起新对话。
369
+ *
370
+ * 安全机制:
371
+ * - 递增 _chatLockToken,让原 chat() 的 finally 块检测到 token 不匹配后
372
+ * 跳过资源清理(避免误清新调用者的 _chatBusy/chatAbortController)
373
+ * - abort chatAbortController,让响应 signal 的 await 点(如 fetch)退出
374
+ * - 原 generator 仍可能在后台运行(无法真正中断不响应 signal 的 await),
375
+ * 但其 finally 块的 token 校验会阻止它影响新调用
376
+ *
377
+ * 幂等性:_chatBusy 已 false 时 no-op(多次调用安全)
378
+ *
379
+ * 使用约束:
380
+ * - 仅在宿主确认 generator 挂起(如无进展超时)后调用
381
+ * - 不应在常规 abort 路径调用(常规 abort 走 signal,generator 自然退出)
382
+ * - 调用后不应再消费原 chat() generator 的后续 chunk(token 已变,chunk 无意义)
383
+ */
384
+ forceReleaseChatLock() {
385
+ if (!this._chatBusy)
386
+ return;
387
+ // 递增 token 让原 chat() 的 finally 块跳过清理(避免误清新调用者的资源)
388
+ this._chatLockToken++;
389
+ // abort 当前 generator(响应 signal 的 await 点会 throw AbortError 退出)
390
+ if (this.chatAbortController) {
391
+ this.chatAbortController.abort();
392
+ this.chatAbortController = null;
393
+ }
394
+ // 清理锁超时定时器(避免后续触发重复清理)
395
+ if (this.chatLockTimer) {
396
+ clearSafeTimeout(this.chatLockTimer);
397
+ this.chatLockTimer = null;
398
+ }
399
+ this._chatBusy = false;
400
+ logger.warn('对话锁被强制释放(宿主无进展超时兜底)');
401
+ }
303
402
  /**
304
403
  * 召回记忆 + 注入最近对话上下文
305
404
  *
@@ -354,24 +453,16 @@ export class Agent extends TypedEventEmitter {
354
453
  *
355
454
  * 所有归档/匹配操作均为 best-effort:任何子步骤失败不应影响用户已收到的回答,
356
455
  * 失败仅记录日志,不向上抛出异常。
456
+ *
457
+ * ADR-015 归档模式控制:
458
+ * - 角色匹配 + 技能匹配不受 archiveMode 影响(每轮都执行,非归档行为)
459
+ * - `manual` 模式跳过所有自动归档(profile + insight),需用户手动调用
460
+ * archiveProfileFacts() / archiveInsight() 触发
461
+ * - `full` / `insights-only` 模式下 profile + insight 都自动归档
462
+ * (会话归档实现后,`insights-only` 将跳过对话原始内容自动归档)
357
463
  */
358
464
  async postProcess(input, assistantContent) {
359
- // 用户画像实时归档(语义解析在 agent/ 层,存储在 memory/ 层)
360
- if (this.#userProfile) {
361
- try {
362
- const turnIndex = `turn-${Date.now()}`;
363
- const facts = extractUserFacts(input, turnIndex);
364
- // FD-22: 注册到 pendingArchives,确保 close() 时等待后台归档完成,避免写入已关闭的存储
365
- const archiveFactsPromise = this.#userProfile.archiveFacts(facts).catch((err) => {
366
- logger.warn({ err }, '用户画像实时归档失败');
367
- });
368
- this.requireNonNull(this.history, 'history').registerPendingArchive(archiveFactsPromise);
369
- }
370
- catch (err) {
371
- logger.warn({ err }, '用户画像归档初始化失败');
372
- }
373
- }
374
- // 角色自动匹配(best-effort:失败不阻塞对话结束)
465
+ // 角色自动匹配(best-effort:失败不阻塞对话结束,非归档行为不受 archiveMode 影响)
375
466
  if (this.personaManager) {
376
467
  try {
377
468
  const matchedPersona = this.personaManager.autoMatch(input);
@@ -393,12 +484,14 @@ export class Agent extends TypedEventEmitter {
393
484
  logger.warn({ err }, '角色自动匹配失败');
394
485
  }
395
486
  }
396
- // 技能关键词匹配(best-effort:失败不阻塞对话结束)
487
+ // 技能关键词匹配(best-effort:失败不阻塞对话结束,非归档行为不受 archiveMode 影响)
397
488
  if (this.skillManager) {
398
489
  try {
399
490
  const match = this.skillManager.match(input);
400
491
  if (match) {
401
492
  this.activeSkill = match.skill.name;
493
+ // 发射 skillMatched 事件:宿主 UI 可据此展示当前激活技能
494
+ this.emit('skillMatched', { skill: match.skill.name, score: match.score });
402
495
  logger.debug({ skill: match.skill.name, score: match.score }, '技能匹配,下一轮注入');
403
496
  }
404
497
  }
@@ -406,14 +499,50 @@ export class Agent extends TypedEventEmitter {
406
499
  logger.warn({ err }, '技能匹配失败');
407
500
  }
408
501
  }
502
+ // ADR-015: manual 模式跳过所有自动归档(profile + insight),
503
+ // 需用户手动调用 archiveProfileFacts/archiveInsight 触发。
504
+ // 角色匹配/技能匹配/AutoConfigRefiner 属"配置学习"行为,非归档,每轮都执行。
505
+ const skipAutoArchive = this.#config.archiveMode === 'manual';
506
+ if (skipAutoArchive) {
507
+ logger.debug({ mode: 'manual' }, '归档模式为 manual,跳过自动归档');
508
+ }
509
+ // 用户画像实时归档(语义解析在 agent/ 层,存储在 memory/ 层)
510
+ // ADR-015: full / insights-only 模式下 profile facts 自动归档
511
+ if (!skipAutoArchive && this.#userProfile) {
512
+ try {
513
+ const turnIndex = `turn-${Date.now()}`;
514
+ const facts = extractUserFacts(input, turnIndex);
515
+ // 注册到 pendingArchives,确保 close() 时等待后台归档完成,避免写入已关闭的存储
516
+ const archiveFactsPromise = this.#userProfile.archiveFacts(facts).then((entries) => {
517
+ // 发射 memoryAdded 事件:仅对已确认且写入存储的条目(confirmed=true)
518
+ for (const entry of entries) {
519
+ if (entry.confirmed) {
520
+ this.emit('memoryAdded', { id: entry.id, source: 'profile', name: entry.value });
521
+ }
522
+ }
523
+ }).catch((err) => {
524
+ logger.warn({ err }, '用户画像实时归档失败');
525
+ });
526
+ this.requireNonNull(this.history, 'history').registerPendingArchive(archiveFactsPromise);
527
+ }
528
+ catch (err) {
529
+ logger.warn({ err }, '用户画像归档初始化失败');
530
+ }
531
+ }
409
532
  // 输入分类 → Insight 提取(委托给 InsightExtractor)
410
- if (this.insightExtractor) {
533
+ // ADR-015: full / insights-only 模式下 insight 自动归档
534
+ if (!skipAutoArchive && this.insightExtractor) {
411
535
  try {
412
536
  const shouldExtract = this.insightExtractor.classify(input);
413
537
  if (shouldExtract === 'extract') {
414
- const p = this.insightExtractor.extract(input, assistantContent).catch((err) => {
538
+ const p = this.insightExtractor.extract(input, assistantContent).then((memories) => {
539
+ // 发射 memoryAdded + insightExtracted 事件:每条写入/更新的 insight 均通知宿主
540
+ for (const memory of memories) {
541
+ this.emit('memoryAdded', { id: memory.id, source: memory.source, name: memory.name });
542
+ this.emit('insightExtracted', { source: memory.source, insight: memory.content });
543
+ }
544
+ }).catch((err) => {
415
545
  logger.warn({ err }, 'Insight 提取失败');
416
- return null;
417
546
  });
418
547
  this.requireNonNull(this.history, 'history').registerPendingArchive(p);
419
548
  }
@@ -422,10 +551,10 @@ export class Agent extends TypedEventEmitter {
422
551
  logger.warn({ err }, 'Insight 提取初始化失败');
423
552
  }
424
553
  }
425
- // V-201: AutoConfigRefiner(模式 3:Agent 智能总结)
554
+ // AutoConfigRefiner(模式 3:Agent 智能总结)
426
555
  if (this.autoConfigRefiner) {
427
556
  try {
428
- // FD-22: 注册到 pendingArchives,确保 close() 时等待后台分析完成,避免写入已关闭的存储
557
+ // 注册到 pendingArchives,确保 close() 时等待后台分析完成,避免写入已关闭的存储
429
558
  const analyzePromise = this.autoConfigRefiner.analyze(input, assistantContent).catch((err) => {
430
559
  logger.warn({ err }, 'AutoConfigRefiner 分析失败');
431
560
  });
@@ -466,7 +595,7 @@ export class Agent extends TypedEventEmitter {
466
595
  */
467
596
  async switchProject(nameOrPath) {
468
597
  this.assertInitialized('switchProject', ['projectManager', 'provider']);
469
- // FD-21: 对话进行中切换项目会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
598
+ // 对话进行中切换项目会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
470
599
  if (this._chatBusy) {
471
600
  throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再切换项目', [
472
601
  '等待上一轮 chat() 的 AsyncGenerator 耗尽',
@@ -482,11 +611,11 @@ export class Agent extends TypedEventEmitter {
482
611
  const projectPath = target ? target.path : nameOrPath;
483
612
  const projectName = target ? target.name : basename(nameOrPath);
484
613
  const newPctx = await pm.initProject(projectPath, projectName, this.#config.configDir);
485
- // A-003: 记录源项目路径(用于事件),切换前 pctx 可能不存在(首次初始化)
614
+ // 记录源项目路径(用于事件),切换前 pctx 可能不存在(首次初始化)
486
615
  const fromProjectPath = this.pctx?.projectPath ?? null;
487
616
  this.pctx = newPctx;
488
617
  await this.rebuildComponentsWithCurrentCtx();
489
- // A-003: 发射项目切换事件(供宿主 UI 刷新项目相关界面)
618
+ // 发射项目切换事件(供宿主 UI 刷新项目相关界面)
490
619
  this.emit('projectSwitched', {
491
620
  from: fromProjectPath,
492
621
  to: projectPath,
@@ -524,20 +653,18 @@ export class Agent extends TypedEventEmitter {
524
653
  this.memoryInspector = result.memoryInspector;
525
654
  this.autoConfigRefiner = result.autoConfigRefiner;
526
655
  this.workProjection = result.workProjection;
527
- // V-101:注入 VectorStore 到 MemoryInspector,启用混合搜索
656
+ this.sessionArchiver = result.sessionArchiver;
657
+ // 绑定冲突检测回调,InsightExtractor 检测到 contradicts 时 emit('conflictDetected')
658
+ // 与 bindGetRecentHistory 同模式:解决 Agent 晚于 InsightExtractor 创建的时序循环依赖
659
+ this.insightExtractor.bindOnConflict((info) => {
660
+ this.emit('conflictDetected', info);
661
+ });
662
+ // 注入 VectorStore 到 MemoryInspector,启用混合搜索
528
663
  if (this.memoryInspector && this.#config.vectorStore) {
529
664
  this.memoryInspector.setVectorStore(this.#config.vectorStore);
530
665
  }
531
- // 创建会话管理器(通过回调访问当前组件,支持 rebuildComponents 后自动获取最新引用)
532
- // 事件转发桥接:SessionManager 使用宽类型 (string, Record<string,unknown>)
533
- // Agent 内部桥接到 TypedEventEmitter 的强类型 emit
534
- // 运行时校验事件名是否在 AgentEventMap 中,避免不安全的类型断言
535
- const forwardEvent = (event, data) => {
536
- if (AGENT_EVENT_NAMES.has(event)) {
537
- this.emit(event, data);
538
- }
539
- };
540
- this._sessionManager = new SessionManager(() => this.requireNonNull(this.history, 'history'), () => this.requireNonNull(this.loop, 'loop'), this.#config.sessionStore, () => this._chatBusy, forwardEvent);
666
+ // 创建会话管理器(提取 createSessionManager 辅助方法,消除重复)
667
+ this._sessionManager = this.createSessionManager();
541
668
  }
542
669
  /**
543
670
  * 用当前 pctx 重建 history / loop
@@ -546,17 +673,37 @@ export class Agent extends TypedEventEmitter {
546
673
  if (!this.pctx)
547
674
  return;
548
675
  await this.assembleComponents(this.pctx);
549
- // 重建会话管理器:assembleComponents 创建了新的 history/loop 实例
550
- // 事件转发桥接(同 assembleComponents 中的逻辑,含运行时校验)
676
+ // 重建会话管理器:assembleComponents 创建了新的 history/loop 实例(复用 createSessionManager)
677
+ this._sessionManager = this.createSessionManager();
678
+ }
679
+ /**
680
+ * 创建会话管理器(提取重复的 forwardEvent + SessionManager 构造逻辑)
681
+ *
682
+ * assembleComponents 和 rebuildComponentsWithCurrentCtx 共享同一套构造逻辑:
683
+ * - 通过回调访问当前组件(支持 rebuild 后自动获取最新引用)
684
+ * - 事件转发桥接:SessionManager 使用宽类型 (string, Record<string,unknown>),
685
+ * Agent 内部桥接到 TypedEventEmitter 的强类型 emit
686
+ * - 运行时校验事件名是否在 AgentEventMap 中,避免不安全的类型断言
687
+ *
688
+ * @returns 新的 SessionManager 实例
689
+ */
690
+ createSessionManager() {
551
691
  const forwardEvent = (event, data) => {
552
692
  if (AGENT_EVENT_NAMES.has(event)) {
553
693
  this.emit(event, data);
554
694
  }
555
695
  };
556
- this._sessionManager = new SessionManager(() => this.requireNonNull(this.history, 'history'), () => this.requireNonNull(this.loop, 'loop'), this.#config.sessionStore, () => this._chatBusy, forwardEvent);
696
+ return new SessionManager(() => this.requireNonNull(this.history, 'history'), () => this.requireNonNull(this.loop, 'loop'), this.#config.sessionStore, () => this._chatBusy, forwardEvent);
557
697
  }
558
698
  // ─── Provider 管理 ────────────────────────────────────
559
699
  setProvider(provider) {
700
+ // 对话进行中切换 Provider 会导致同一 processUserInput 循环内前后两次 LLM 调用命中不同 Provider
701
+ // (模型上下文窗口假设不一致 → 可能导致上下文截断逻辑误判或 tool_call 格式不兼容)
702
+ if (this._chatBusy) {
703
+ throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再切换 Provider', [
704
+ '等待上一轮 chat() 的 AsyncGenerator 耗尽',
705
+ ]);
706
+ }
560
707
  this.#provider = provider;
561
708
  if (this.loop) {
562
709
  this.loop.setProvider(provider);
@@ -564,13 +711,145 @@ export class Agent extends TypedEventEmitter {
564
711
  logger.info({ provider: this.#provider.name }, 'Provider 已切换');
565
712
  }
566
713
  setBackgroundProvider(provider) {
714
+ // 与 setProvider 一致,对话进行中禁止切换后台 Provider
715
+ if (this._chatBusy) {
716
+ throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再切换后台 Provider', [
717
+ '等待上一轮 chat() 的 AsyncGenerator 耗尽',
718
+ ]);
719
+ }
567
720
  this.#backgroundProvider = provider;
568
- // V-201: 同步更新 AutoConfigRefiner 的后台 Provider
721
+ // 同步更新 AutoConfigRefiner 的后台 Provider
569
722
  if (this.autoConfigRefiner) {
570
723
  this.autoConfigRefiner.setBackgroundProvider(provider);
571
724
  }
572
725
  logger.info({ hasBackground: !!provider }, '后台 Provider 已切换');
573
726
  }
727
+ // ─── 归档模式管理(ADR-015) ───────────────────────────
728
+ /**
729
+ * 运行时切换归档模式
730
+ *
731
+ * 与 setProvider 一致,对话进行中禁止切换(避免本轮 postProcess 行为不一致)。
732
+ *
733
+ * @param mode 目标模式
734
+ */
735
+ setArchiveMode(mode) {
736
+ if (this._chatBusy) {
737
+ throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再切换归档模式', [
738
+ '等待上一轮 chat() 的 AsyncGenerator 耗尽',
739
+ ]);
740
+ }
741
+ const prev = this.#config.archiveMode;
742
+ if (prev === mode)
743
+ return; // 幂等:无变更直接返回
744
+ this.#config.archiveMode = mode;
745
+ logger.info({ from: prev, to: mode }, '归档模式已切换');
746
+ }
747
+ /**
748
+ * 查询当前归档模式
749
+ *
750
+ * 宿主 UI(如设置面板)可据此同步显示当前模式。
751
+ */
752
+ getArchiveMode() {
753
+ return this.#config.archiveMode;
754
+ }
755
+ /**
756
+ * 手动触发 profile facts 归档(manual 模式下使用)
757
+ *
758
+ * manual 模式下 postProcess 跳过自动归档,用户需通过此 API 主动归档。
759
+ * full / insights-only 模式下也可调用(会重复归档,但不推荐)。
760
+ *
761
+ * 归档逻辑已委托给 ArchiveCoordinator
762
+ *
763
+ * @param input 本轮用户输入
764
+ * @returns 写入/更新的 UserProfileEntry 列表
765
+ */
766
+ async archiveProfileFacts(input) {
767
+ this.assertInitialized('archiveProfileFacts');
768
+ return this.archiveCoordinator.archiveProfileFacts(input);
769
+ }
770
+ /**
771
+ * 手动触发 insight 提取(manual 模式下使用)
772
+ *
773
+ * manual 模式下 postProcess 跳过自动归档,用户需通过此 API 主动归档。
774
+ * 内部仍走 classify 判断(避免无价值输入浪费 LLM 调用)。
775
+ *
776
+ * 归档逻辑已委托给 ArchiveCoordinator
777
+ *
778
+ * @param input 本轮用户输入
779
+ * @param assistantContent 本轮助手回复内容
780
+ * @returns 写入/更新的 Memory 列表
781
+ */
782
+ async archiveInsight(input, assistantContent) {
783
+ this.assertInitialized('archiveInsight');
784
+ return this.archiveCoordinator.archiveInsight(input, assistantContent);
785
+ }
786
+ /**
787
+ * 手动归档会话内容(content 类记忆)
788
+ *
789
+ * 适用于 `insights-only` / `manual` 模式下用户手动触发会话内容归档。
790
+ * `full` 模式下由宿主在会话切换前自动调用,无需用户干预。
791
+ *
792
+ * 归档逻辑已委托给 ArchiveCoordinator
793
+ *
794
+ * @param date 会话日期 YYYY-MM-DD
795
+ * @param session 会话标识(不含日期前缀)
796
+ * @returns 归档结果(memories 可能为空,表示无归档价值或 LLM 失败)
797
+ */
798
+ async archiveSessionContent(date, session) {
799
+ this.assertInitialized('archiveSessionContent');
800
+ return this.archiveCoordinator.archiveSessionContent(date, session);
801
+ }
802
+ // ─── 配置重载(事件驱动) ───────────────────────
803
+ /**
804
+ * 重载配置类记忆:从 configDir 重新扫描指定 source 的配置文件并更新内存缓存 + SQLite 索引
805
+ *
806
+ * 解决方案:installSkill 写入文件后或 confirmConfigSuggestion 写入配置文件后,
807
+ * 调用此方法使当前会话立即生效,无需重启 Agent。
808
+ *
809
+ * 支持的 source:
810
+ * - 'skill' → SkillManager.reload() 清空缓存重新扫描 skills/ 目录
811
+ * - 'persona' → PersonaManager.reload() 清空缓存重新扫描 personas/ 目录(保持激活角色)
812
+ * - 'rule' → 无操作(rule 类型已由 ConfigManager.addRule() 即时注入 system prompt)
813
+ * - 'guardrail' → 抛错(guardrail 是 AgentLoop 的 readonly 数组,需 rebuildComponents 才能重载)
814
+ * - undefined → 重载 skill + persona(全量重载,不含 guardrail)
815
+ *
816
+ * @param source 配置类型,缺省时重载全部可热更新的配置
817
+ * @returns 重载结果统计
818
+ */
819
+ async reloadConfig(source) {
820
+ this.assertInitialized('reloadConfig');
821
+ if (this._chatBusy) {
822
+ throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再重载配置', [
823
+ '等待上一轮 chat() 的 AsyncGenerator 耗尽',
824
+ ]);
825
+ }
826
+ // guardrail 需重建 AgentLoop,不属于热重载范畴
827
+ if (source === 'guardrail') {
828
+ throw configError('guardrail 不支持热重载', 'guardrail 规则是 AgentLoop 的 readonly 数组,需调用 rebuildComponents() 重建', ['使用 rebuildComponents() 重建组件(代价较高)']);
829
+ }
830
+ // rule 类型已由 ConfigManager.addRule() 即时注入,无需重载
831
+ if (source === 'rule') {
832
+ logger.info('rule 类型已由 addRule() 即时注入,reloadConfig 跳过');
833
+ return { skill: 0, persona: 0 };
834
+ }
835
+ const result = { skill: 0, persona: 0 };
836
+ // 按需重载:source 缺省时全量重载,否则只重载指定类型
837
+ const shouldReloadSkill = !source || source === 'skill';
838
+ const shouldReloadPersona = !source || source === 'persona';
839
+ if (shouldReloadSkill && this.skillManager) {
840
+ result.skill = await this.skillManager.reload();
841
+ }
842
+ if (shouldReloadPersona && this.personaManager) {
843
+ result.persona = await this.personaManager.reload();
844
+ // 角色重载后,刷新 AgentLoop 的角色前缀(使新角色内容立即注入 system prompt)
845
+ if (this.loop) {
846
+ const newPrefix = this.personaManager.buildSystemPrompt();
847
+ this.loop.refreshPersonaPrefix(newPrefix);
848
+ }
849
+ }
850
+ logger.info({ source, ...result }, '配置已热重载');
851
+ return result;
852
+ }
574
853
  // ─── 组件访问 ─────────────────────────────────────────
575
854
  /**
576
855
  * 重建内部组件(history / loop / managers)
@@ -579,7 +858,7 @@ export class Agent extends TypedEventEmitter {
579
858
  * 仅在宿主项目需要强制刷新组件时使用(如热更新配置后)。
580
859
  */
581
860
  async rebuildComponents() {
582
- // FD-21: 对话进行中重建组件会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
861
+ // 对话进行中重建组件会导致 loop/history 引用被替换,工作记忆与持久化状态不一致
583
862
  if (this._chatBusy) {
584
863
  throw configError('对话繁忙', '上一轮对话尚未完成,请等待其结束后再重建组件', [
585
864
  '等待上一轮 chat() 的 AsyncGenerator 耗尽',
@@ -628,45 +907,22 @@ export class Agent extends TypedEventEmitter {
628
907
  return value;
629
908
  }
630
909
  // ─── 记忆生命周期 ───────────────────────────────────────
631
- /**
632
- * 对 insight/profile/work-projection 记忆执行 score 衰减
633
- *
634
- * 长期未访问的记忆 score 逐渐降低,体现"自然遗忘"
635
- * 不影响 persona/rule/skill(这些是配置型记忆,不应衰减)
636
- *
637
- * 衰减逻辑委派给 IMemoryStorage.decayScores(),
638
- * 宿主(SqliteStorage)可用一条 SQL UPDATE 批量完成,避免 O(n) 全量加载。
639
- */
640
- runMemoryDecay() {
641
- if (!this.pctx)
642
- return;
643
- // R-103 衰减 Span:记录衰减执行过程,补全衰减可观测性缺口
644
- const decaySpan = this.#config.tracer?.startSpan(TRACE_SPANS.DECAY) ?? NOOP_TRACER.startSpan(TRACE_SPANS.DECAY);
645
- try {
646
- const sources = [SOURCE_LABELS.INSIGHT, SOURCE_LABELS.PROFILE, SOURCE_LABELS.WORK_PROJECTION];
647
- const decayedCount = this.pctx.index.decayScores(sources, new Date());
648
- logger.debug({ decayedCount }, '记忆衰减完成');
649
- // R-103 衰减指标统计:累计执行次数和衰减记忆数
650
- this.metricDecayRunCount++;
651
- this.metricTotalDecayedCount += decayedCount;
652
- this.metricLastDecayAt = new Date().toISOString();
653
- decaySpan.setAttribute('decayedCount', decayedCount);
654
- decaySpan.setAttribute('totalRuns', this.metricDecayRunCount);
655
- this.emit('decayCompleted', { decayedCount });
656
- }
657
- catch (err) {
658
- logger.warn({ err }, '记忆衰减异常,跳过本轮');
659
- decaySpan.recordException(toError(err));
660
- }
661
- finally {
662
- decaySpan.end();
663
- }
664
- }
910
+ // ─── runMemoryDecay 已迁移至 MemoryDecayScheduler.runOnce ─────
665
911
  // ─── 关闭 ─────────────────────────────────────────────
666
912
  /**
667
913
  * 关闭 Agent,释放 SQLite 连接等资源
668
914
  */
669
915
  async close() {
916
+ // 递增 token,使任何进行中的 chat() generator 的 finally 块
917
+ // 检测到 token 变化后跳过资源清理(close 已接管清理职责)
918
+ this._chatLockToken++;
919
+ // 清理 MemoryDecayScheduler(含定时器和 storage 引用)
920
+ if (this.memoryDecayScheduler) {
921
+ this.memoryDecayScheduler.stop();
922
+ this.memoryDecayScheduler = null;
923
+ }
924
+ // 清理 ArchiveCoordinator(无定时器,只需释放引用)
925
+ this.archiveCoordinator = null;
670
926
  // 清理定时器
671
927
  if (this.decayTimer) {
672
928
  clearSafeInterval(this.decayTimer);
@@ -680,13 +936,13 @@ export class Agent extends TypedEventEmitter {
680
936
  this.chatAbortController.abort();
681
937
  this.chatAbortController = null;
682
938
  }
683
- // P2-1 清理 PersonaManager 的角色切换防抖锁计时器,防止关闭后回调触发
939
+ // 清理 PersonaManager 的角色切换防抖锁计时器,防止关闭后回调触发
684
940
  if (this.personaManager) {
685
941
  this.personaManager.close();
686
942
  }
687
943
  this.removeAllListeners();
688
944
  if (this.history) {
689
- await this.history.awaitPendingArchives(5000);
945
+ await this.history.awaitPendingArchives(AGENT_CONSTANTS.SHUTDOWN_ARCHIVE_TIMEOUT_MS);
690
946
  }
691
947
  if (this.projectManager) {
692
948
  await this.projectManager.shutdown();
@@ -702,7 +958,19 @@ export class Agent extends TypedEventEmitter {
702
958
  this.configManager = null;
703
959
  this.memoryInspector = null;
704
960
  this.autoConfigRefiner = null;
961
+ this.sessionArchiver = null;
705
962
  this.pctx = null;
963
+ // 补全剩余 manager 字段 null 化,与上述字段处理方式一致
964
+ // (原实现仅 null 化部分 manager,toolExec/personaManager/#userProfile/skillManager/workProjection 遗漏)
965
+ this.toolExec = null;
966
+ this.personaManager = null;
967
+ this.#userProfile = null;
968
+ this.skillManager = null;
969
+ this.workProjection = null;
970
+ // 清理次要状态字段,防止 re-init 后残留上一会话状态
971
+ this.activeSkill = null;
972
+ this._lastInteractionAt = null;
973
+ // 衰减指标已迁移至 MemoryDecayScheduler,close 时通过 stop() 销毁实例
706
974
  }
707
975
  // ─── 只读访问器 ───────────────────────────────────────
708
976
  get initialized() {
@@ -737,9 +1005,9 @@ export class Agent extends TypedEventEmitter {
737
1005
  get lastInteractionAt() {
738
1006
  return this._lastInteractionAt;
739
1007
  }
740
- // ─── R-103 运行时指标 ────────────────────────────────
1008
+ // ─── 运行时指标 ────────────────────────────────
741
1009
  /**
742
- * 获取 Agent 运行时指标快照(R-103 可观测性增强)
1010
+ * 获取 Agent 运行时指标快照(可观测性增强)
743
1011
  *
744
1012
  * 聚合 AgentLoop 指标(LLM 调用、记忆召回、工具调用、上下文管理)
745
1013
  * 与 Agent 层指标(记忆衰减),返回完整的 AgentMetrics 快照。
@@ -754,6 +1022,12 @@ export class Agent extends TypedEventEmitter {
754
1022
  * @returns AgentMetrics 完整快照(含衰减指标)
755
1023
  */
756
1024
  getMetrics() {
1025
+ // 衰减指标从 MemoryDecayScheduler 读取
1026
+ const decayMetrics = this.memoryDecayScheduler?.getMetrics() ?? {
1027
+ runCount: 0,
1028
+ totalDecayedCount: 0,
1029
+ lastRunAt: null,
1030
+ };
757
1031
  // 未初始化时返回全零指标,避免调用方需要判空
758
1032
  if (!this.loop) {
759
1033
  return {
@@ -761,22 +1035,14 @@ export class Agent extends TypedEventEmitter {
761
1035
  recall: { totalCount: 0, hitCount: 0, hitRate: 0 },
762
1036
  tools: { callCount: 0, failureCount: 0 },
763
1037
  context: { truncationCount: 0, messageCount: 0, estimatedTokens: 0 },
764
- decay: {
765
- runCount: this.metricDecayRunCount,
766
- totalDecayedCount: this.metricTotalDecayedCount,
767
- lastRunAt: this.metricLastDecayAt,
768
- },
1038
+ decay: decayMetrics,
769
1039
  };
770
1040
  }
771
1041
  // 获取 AgentLoop 指标快照,填充衰减字段
772
1042
  const loopMetrics = this.loop.getMetrics();
773
1043
  return {
774
1044
  ...loopMetrics,
775
- decay: {
776
- runCount: this.metricDecayRunCount,
777
- totalDecayedCount: this.metricTotalDecayedCount,
778
- lastRunAt: this.metricLastDecayAt,
779
- },
1045
+ decay: decayMetrics,
780
1046
  };
781
1047
  }
782
1048
  // ─── Manager 暴露(激进拆分:调用方直接操作 Manager)──
@@ -885,9 +1151,5 @@ export class Agent extends TypedEventEmitter {
885
1151
  get sessionManager() {
886
1152
  return this._sessionManager;
887
1153
  }
888
- /** 记忆存储(宿主可直接调用 CRUD,如 delete/upsert) */
889
- get storage() {
890
- return this.#config.storage ?? null;
891
- }
892
1154
  }
893
1155
  //# sourceMappingURL=agent.js.map