berry-agent 0.1.0-alpha.1

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 (401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/.api-emit.stamp +1 -0
  4. package/dist/.build-meta.json +6 -0
  5. package/dist/agent/events.js +1 -0
  6. package/dist/agent/index.js +2 -0
  7. package/dist/agent/loop.js +139 -0
  8. package/dist/agent/queue.js +119 -0
  9. package/dist/agent/stream.js +62 -0
  10. package/dist/agent/tools-batch.js +221 -0
  11. package/dist/agent/types.js +8 -0
  12. package/dist/api/berry-agent-llm.d.ts +18 -0
  13. package/dist/api/berry-agent.d.ts +9 -0
  14. package/dist/api/surface.json +1632 -0
  15. package/dist/api/tsconfig.paths.json +12 -0
  16. package/dist/api/typebox-compile.d.ts +2 -0
  17. package/dist/api/typebox-value.d.ts +2 -0
  18. package/dist/api/typebox.d.ts +2 -0
  19. package/dist/browser/cdp.js +256 -0
  20. package/dist/browser/codes.js +31 -0
  21. package/dist/browser/discover.js +107 -0
  22. package/dist/browser/engine.js +169 -0
  23. package/dist/browser/index.js +17 -0
  24. package/dist/browser/install.js +342 -0
  25. package/dist/browser/page.js +321 -0
  26. package/dist/browser/service.js +158 -0
  27. package/dist/browser/tools.js +198 -0
  28. package/dist/browser/types.js +81 -0
  29. package/dist/channels/ask-queue.js +82 -0
  30. package/dist/channels/commands.js +129 -0
  31. package/dist/channels/engine/cell.js +143 -0
  32. package/dist/channels/engine/diff.js +143 -0
  33. package/dist/channels/engine/engine.js +416 -0
  34. package/dist/channels/engine/index.js +8 -0
  35. package/dist/channels/engine/input-keys.js +86 -0
  36. package/dist/channels/engine/input.js +596 -0
  37. package/dist/channels/engine/memory-io.js +71 -0
  38. package/dist/channels/engine/process-io.js +38 -0
  39. package/dist/channels/engine/types.js +17 -0
  40. package/dist/channels/engine/width.js +122 -0
  41. package/dist/channels/index.js +15 -0
  42. package/dist/channels/registry.js +56 -0
  43. package/dist/channels/sdk/admit.js +29 -0
  44. package/dist/channels/sdk/backend.js +102 -0
  45. package/dist/channels/sdk/cursor.js +56 -0
  46. package/dist/channels/sdk/index.js +20 -0
  47. package/dist/channels/sdk/jsonl.js +208 -0
  48. package/dist/channels/sdk/protocol.js +24 -0
  49. package/dist/channels/sdk/schema.js +91 -0
  50. package/dist/channels/sdk/wire-core.js +446 -0
  51. package/dist/channels/service.js +169 -0
  52. package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
  53. package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
  54. package/dist/channels/tui/autocomplete/popup.js +148 -0
  55. package/dist/channels/tui/autocomplete/provider.js +7 -0
  56. package/dist/channels/tui/autocomplete/token.js +68 -0
  57. package/dist/channels/tui/backend/ansi-rows.js +211 -0
  58. package/dist/channels/tui/backend/main-screen.js +207 -0
  59. package/dist/channels/tui/backend/osc.js +91 -0
  60. package/dist/channels/tui/backend/transcript.js +230 -0
  61. package/dist/channels/tui/backend/tui-backend.js +915 -0
  62. package/dist/channels/tui/editor/editor-model.js +543 -0
  63. package/dist/channels/tui/editor/editor-view.js +140 -0
  64. package/dist/channels/tui/editor/editor.js +275 -0
  65. package/dist/channels/tui/editor/undo-stack.js +35 -0
  66. package/dist/channels/tui/editor/visual-lines.js +126 -0
  67. package/dist/channels/tui/editor/word-nav.js +106 -0
  68. package/dist/channels/tui/history/history-viewer.js +403 -0
  69. package/dist/channels/tui/index.js +3 -0
  70. package/dist/channels/tui/layout.js +167 -0
  71. package/dist/channels/tui/markdown/blocks.js +111 -0
  72. package/dist/channels/tui/markdown/inline.js +75 -0
  73. package/dist/channels/tui/markdown/markdown.js +182 -0
  74. package/dist/channels/tui/memory/memory-viewer.js +540 -0
  75. package/dist/channels/tui/overlay/alt-screen.js +105 -0
  76. package/dist/channels/tui/overlay/overlay.js +57 -0
  77. package/dist/channels/tui/overlay/select-confirm.js +156 -0
  78. package/dist/channels/tui/panels/todo-panel.js +54 -0
  79. package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
  80. package/dist/channels/tui/scroll/scroll-view.js +254 -0
  81. package/dist/channels/tui/status/status-line.js +76 -0
  82. package/dist/channels/tui/text.js +38 -0
  83. package/dist/channels/tui/theme.js +41 -0
  84. package/dist/channels/types.js +11 -0
  85. package/dist/channels/ui-core.js +209 -0
  86. package/dist/checkpoint/capture.js +45 -0
  87. package/dist/checkpoint/codes.js +33 -0
  88. package/dist/checkpoint/command.js +82 -0
  89. package/dist/checkpoint/gate.js +48 -0
  90. package/dist/checkpoint/index.js +20 -0
  91. package/dist/checkpoint/restore.js +172 -0
  92. package/dist/checkpoint/store.js +247 -0
  93. package/dist/checkpoint/types.js +4 -0
  94. package/dist/checkpoint/walk.js +146 -0
  95. package/dist/compaction/ccr-tools.js +112 -0
  96. package/dist/compaction/ccr.js +84 -0
  97. package/dist/compaction/codes.js +21 -0
  98. package/dist/compaction/index.js +18 -0
  99. package/dist/compaction/policy.js +200 -0
  100. package/dist/compaction/service.js +422 -0
  101. package/dist/compaction/slots.js +121 -0
  102. package/dist/compaction/types.js +10 -0
  103. package/dist/context/codes.js +40 -0
  104. package/dist/context/events.js +160 -0
  105. package/dist/context/index.js +13 -0
  106. package/dist/context/logger.js +136 -0
  107. package/dist/context/scope.js +182 -0
  108. package/dist/context/workspace.js +104 -0
  109. package/dist/contracts/agent-events.d.ts +73 -0
  110. package/dist/contracts/agent-events.js +16 -0
  111. package/dist/contracts/api.d.ts +249 -0
  112. package/dist/contracts/api.js +299 -0
  113. package/dist/contracts/approval.d.ts +37 -0
  114. package/dist/contracts/approval.js +9 -0
  115. package/dist/contracts/env-ref.d.ts +41 -0
  116. package/dist/contracts/env-ref.js +32 -0
  117. package/dist/contracts/errors.d.ts +56 -0
  118. package/dist/contracts/errors.js +266 -0
  119. package/dist/contracts/events.d.ts +70 -0
  120. package/dist/contracts/events.js +329 -0
  121. package/dist/contracts/index.d.ts +32 -0
  122. package/dist/contracts/index.js +26 -0
  123. package/dist/contracts/llm.d.ts +275 -0
  124. package/dist/contracts/llm.js +12 -0
  125. package/dist/contracts/messages.d.ts +63 -0
  126. package/dist/contracts/messages.js +65 -0
  127. package/dist/contracts/redact.d.ts +46 -0
  128. package/dist/contracts/redact.js +223 -0
  129. package/dist/contracts/tools.d.ts +197 -0
  130. package/dist/contracts/tools.js +33 -0
  131. package/dist/contracts/types.d.ts +302 -0
  132. package/dist/contracts/types.js +50 -0
  133. package/dist/contracts/ui.d.ts +122 -0
  134. package/dist/contracts/ui.js +15 -0
  135. package/dist/conversation/agent-service.js +61 -0
  136. package/dist/conversation/approval-wiring.js +82 -0
  137. package/dist/conversation/backoff.js +38 -0
  138. package/dist/conversation/codes.js +47 -0
  139. package/dist/conversation/control-tools.js +102 -0
  140. package/dist/conversation/control.js +210 -0
  141. package/dist/conversation/driver.js +922 -0
  142. package/dist/conversation/index.js +12 -0
  143. package/dist/conversation/model-visible.js +169 -0
  144. package/dist/conversation/open-tools.js +126 -0
  145. package/dist/conversation/reseed.js +132 -0
  146. package/dist/conversation/sessions.js +215 -0
  147. package/dist/conversation/todo.js +203 -0
  148. package/dist/conversation/types.js +35 -0
  149. package/dist/conversation/wiring.js +204 -0
  150. package/dist/credentials/codes.js +59 -0
  151. package/dist/credentials/commands.js +186 -0
  152. package/dist/credentials/env-ref.js +34 -0
  153. package/dist/credentials/index.js +28 -0
  154. package/dist/credentials/migration.js +18 -0
  155. package/dist/credentials/oauth.js +266 -0
  156. package/dist/credentials/refresh.js +152 -0
  157. package/dist/credentials/secrets.js +118 -0
  158. package/dist/credentials/types.js +30 -0
  159. package/dist/exec/bash.js +279 -0
  160. package/dist/exec/codes.js +37 -0
  161. package/dist/exec/env.js +99 -0
  162. package/dist/exec/environment.js +39 -0
  163. package/dist/exec/git-guard.js +623 -0
  164. package/dist/exec/index.js +24 -0
  165. package/dist/exec/registry.js +120 -0
  166. package/dist/exec/spawn.js +281 -0
  167. package/dist/exec/tail.js +77 -0
  168. package/dist/exec/types.js +5 -0
  169. package/dist/goal/codes.js +33 -0
  170. package/dist/goal/command.js +175 -0
  171. package/dist/goal/fold.js +121 -0
  172. package/dist/goal/gates.js +122 -0
  173. package/dist/goal/index.js +22 -0
  174. package/dist/goal/migration.js +48 -0
  175. package/dist/goal/service.js +501 -0
  176. package/dist/goal/todo-tool.js +174 -0
  177. package/dist/goal/types.js +1 -0
  178. package/dist/goal/update-tool.js +46 -0
  179. package/dist/host/approval-cmd.js +276 -0
  180. package/dist/host/assembly.js +1146 -0
  181. package/dist/host/boot-failures.js +104 -0
  182. package/dist/host/budget-advisory.js +74 -0
  183. package/dist/host/budget-broadcast.js +50 -0
  184. package/dist/host/builtins.js +54 -0
  185. package/dist/host/cli.js +544 -0
  186. package/dist/host/codes.js +120 -0
  187. package/dist/host/config-schema.js +195 -0
  188. package/dist/host/conversation-stack.js +654 -0
  189. package/dist/host/core-plugins.js +1685 -0
  190. package/dist/host/credentials-cmd.js +42 -0
  191. package/dist/host/disclosure.js +30 -0
  192. package/dist/host/dispatch.js +105 -0
  193. package/dist/host/doors-cmd.js +154 -0
  194. package/dist/host/dump-config.js +44 -0
  195. package/dist/host/hook-dispatch-guard.js +27 -0
  196. package/dist/host/import-gate.js +153 -0
  197. package/dist/host/index.js +54 -0
  198. package/dist/host/issue-session.js +241 -0
  199. package/dist/host/loader.js +372 -0
  200. package/dist/host/main.js +161 -0
  201. package/dist/host/manifest.js +381 -0
  202. package/dist/host/mcp-entry.js +53 -0
  203. package/dist/host/plugin-boot.js +952 -0
  204. package/dist/host/plugin-context.js +672 -0
  205. package/dist/host/plugin-install.js +480 -0
  206. package/dist/host/plugin-reload.js +124 -0
  207. package/dist/host/plugin-store.js +462 -0
  208. package/dist/host/plugin-tools.js +359 -0
  209. package/dist/host/plugin-uninstall.js +257 -0
  210. package/dist/host/plugins-cmd.js +302 -0
  211. package/dist/host/plugins-command.js +130 -0
  212. package/dist/host/plugins-config.js +195 -0
  213. package/dist/host/prompt-sections.js +133 -0
  214. package/dist/host/run-entry.js +544 -0
  215. package/dist/host/runtime.js +209 -0
  216. package/dist/host/scheduler-tick.js +294 -0
  217. package/dist/host/serve-daemon.js +501 -0
  218. package/dist/host/serve-entry.js +279 -0
  219. package/dist/host/session-anchor.js +41 -0
  220. package/dist/host/sessions-cmd.js +219 -0
  221. package/dist/host/sessions-face.js +47 -0
  222. package/dist/host/settings-store.js +112 -0
  223. package/dist/host/signals.js +69 -0
  224. package/dist/host/single-instance.js +105 -0
  225. package/dist/host/subagent-factory.js +291 -0
  226. package/dist/host/testkit/harness.js +283 -0
  227. package/dist/host/testkit/index.js +14 -0
  228. package/dist/host/testkit/matrix.js +302 -0
  229. package/dist/host/tool-policy-store.js +177 -0
  230. package/dist/host/triggers.js +212 -0
  231. package/dist/host/tui-entry.js +216 -0
  232. package/dist/host/webui-bridge.js +165 -0
  233. package/dist/issue/codes.js +38 -0
  234. package/dist/issue/filter.js +146 -0
  235. package/dist/issue/github.js +116 -0
  236. package/dist/issue/index.js +17 -0
  237. package/dist/issue/mount.js +70 -0
  238. package/dist/issue/poll.js +58 -0
  239. package/dist/issue/service.js +341 -0
  240. package/dist/issue/tools.js +70 -0
  241. package/dist/issue/types.js +34 -0
  242. package/dist/issue/webhook.js +126 -0
  243. package/dist/llm/codes.js +77 -0
  244. package/dist/llm/complete.js +196 -0
  245. package/dist/llm/events.js +15 -0
  246. package/dist/llm/index.js +24 -0
  247. package/dist/llm/inflight.js +65 -0
  248. package/dist/llm/model-id.js +58 -0
  249. package/dist/llm/provider-face.d.ts +32 -0
  250. package/dist/llm/provider-face.js +34 -0
  251. package/dist/llm/recovery.js +146 -0
  252. package/dist/llm/runtime.js +46 -0
  253. package/dist/llm/stream-fn.js +147 -0
  254. package/dist/lsp/codes.js +31 -0
  255. package/dist/lsp/connection.js +147 -0
  256. package/dist/lsp/frame.js +110 -0
  257. package/dist/lsp/index.js +16 -0
  258. package/dist/lsp/inject.js +58 -0
  259. package/dist/lsp/instance.js +264 -0
  260. package/dist/lsp/service.js +522 -0
  261. package/dist/lsp/tools.js +53 -0
  262. package/dist/lsp/types.js +201 -0
  263. package/dist/mcp/bridge.js +222 -0
  264. package/dist/mcp/codes.js +21 -0
  265. package/dist/mcp/index.js +19 -0
  266. package/dist/mcp/jsonrpc.js +230 -0
  267. package/dist/mcp/service.js +125 -0
  268. package/dist/mcp/tools.js +145 -0
  269. package/dist/mcp/types.js +108 -0
  270. package/dist/memory/cite.js +131 -0
  271. package/dist/memory/codes.js +50 -0
  272. package/dist/memory/command.js +71 -0
  273. package/dist/memory/consolidate.js +219 -0
  274. package/dist/memory/cycle.js +109 -0
  275. package/dist/memory/dao.js +878 -0
  276. package/dist/memory/diff.js +238 -0
  277. package/dist/memory/extract.js +181 -0
  278. package/dist/memory/fts.js +58 -0
  279. package/dist/memory/index.js +42 -0
  280. package/dist/memory/inject.js +354 -0
  281. package/dist/memory/merge.js +178 -0
  282. package/dist/memory/migration.js +132 -0
  283. package/dist/memory/pollution.js +59 -0
  284. package/dist/memory/port.js +385 -0
  285. package/dist/memory/review.js +201 -0
  286. package/dist/memory/scan.js +89 -0
  287. package/dist/memory/tools.js +508 -0
  288. package/dist/memory/types.js +140 -0
  289. package/dist/obs/codes.js +38 -0
  290. package/dist/obs/db.js +115 -0
  291. package/dist/obs/index.js +20 -0
  292. package/dist/obs/rollup.js +61 -0
  293. package/dist/obs/service.js +363 -0
  294. package/dist/obs/session-tools.js +282 -0
  295. package/dist/obs/session-view.js +225 -0
  296. package/dist/obs/tool.js +97 -0
  297. package/dist/obs/types.js +1 -0
  298. package/dist/persist/audit.js +63 -0
  299. package/dist/persist/aux.js +39 -0
  300. package/dist/persist/codes.js +35 -0
  301. package/dist/persist/index.js +30 -0
  302. package/dist/persist/load-history.js +50 -0
  303. package/dist/persist/migrations.js +37 -0
  304. package/dist/persist/paths.js +108 -0
  305. package/dist/persist/persistence.js +229 -0
  306. package/dist/persist/schema.js +99 -0
  307. package/dist/persist/secret-box.js +95 -0
  308. package/dist/persist/store.js +706 -0
  309. package/dist/persist/write-behind.js +238 -0
  310. package/dist/safety/approval.js +171 -0
  311. package/dist/safety/bwrap.js +123 -0
  312. package/dist/safety/codes.js +57 -0
  313. package/dist/safety/danger.js +505 -0
  314. package/dist/safety/gate.js +226 -0
  315. package/dist/safety/index.js +24 -0
  316. package/dist/safety/presets.js +73 -0
  317. package/dist/safety/roots.js +139 -0
  318. package/dist/safety/sandbox.js +214 -0
  319. package/dist/safety/seatbelt.js +88 -0
  320. package/dist/safety/sensitive.js +34 -0
  321. package/dist/safety/tool-policy.js +203 -0
  322. package/dist/safety/types.js +13 -0
  323. package/dist/scheduler/codes.js +54 -0
  324. package/dist/scheduler/cron-backend.js +135 -0
  325. package/dist/scheduler/engine.js +280 -0
  326. package/dist/scheduler/gates.js +60 -0
  327. package/dist/scheduler/index.js +25 -0
  328. package/dist/scheduler/migration.js +22 -0
  329. package/dist/scheduler/runner.js +171 -0
  330. package/dist/scheduler/schedule.js +193 -0
  331. package/dist/scheduler/service.js +283 -0
  332. package/dist/scheduler/tick.js +139 -0
  333. package/dist/scheduler/types.js +1 -0
  334. package/dist/sdk/http.js +865 -0
  335. package/dist/sdk/index.js +22 -0
  336. package/dist/sdk/mcp.js +289 -0
  337. package/dist/sdk/plugin-route-registry.js +126 -0
  338. package/dist/sdk/plugin-routes.js +149 -0
  339. package/dist/sdk/security.js +116 -0
  340. package/dist/sdk/types.js +32 -0
  341. package/dist/session/budget.js +159 -0
  342. package/dist/session/codes.js +40 -0
  343. package/dist/session/derive.js +180 -0
  344. package/dist/session/event-data.js +1 -0
  345. package/dist/session/fork.js +72 -0
  346. package/dist/session/import-gates.js +148 -0
  347. package/dist/session/index.js +18 -0
  348. package/dist/session/recover.js +103 -0
  349. package/dist/session/session.js +279 -0
  350. package/dist/session/snapshot.js +107 -0
  351. package/dist/skills/agents.js +242 -0
  352. package/dist/skills/codes.js +51 -0
  353. package/dist/skills/discovery.js +266 -0
  354. package/dist/skills/frontmatter.js +215 -0
  355. package/dist/skills/index.js +28 -0
  356. package/dist/skills/load.js +118 -0
  357. package/dist/skills/manage.js +201 -0
  358. package/dist/skills/registry.js +138 -0
  359. package/dist/skills/render.js +124 -0
  360. package/dist/skills/sections.js +171 -0
  361. package/dist/skills/types.js +21 -0
  362. package/dist/subagent/codes.js +50 -0
  363. package/dist/subagent/declarative.js +55 -0
  364. package/dist/subagent/index.js +19 -0
  365. package/dist/subagent/notify.js +51 -0
  366. package/dist/subagent/provide.js +7 -0
  367. package/dist/subagent/registry.js +149 -0
  368. package/dist/subagent/service.js +338 -0
  369. package/dist/subagent/surface.js +44 -0
  370. package/dist/subagent/tool.js +153 -0
  371. package/dist/subagent/types.js +25 -0
  372. package/dist/tools/apply-patch.js +164 -0
  373. package/dist/tools/codes.js +109 -0
  374. package/dist/tools/fs.js +501 -0
  375. package/dist/tools/index.js +17 -0
  376. package/dist/tools/observed.js +99 -0
  377. package/dist/tools/pipeline.js +176 -0
  378. package/dist/tools/protected-read.js +54 -0
  379. package/dist/tools/registry.js +250 -0
  380. package/dist/tools/search.js +451 -0
  381. package/dist/tools/worktree.js +327 -0
  382. package/dist/web/codes.js +37 -0
  383. package/dist/web/gate.js +43 -0
  384. package/dist/web/hygiene.js +195 -0
  385. package/dist/web/index.js +16 -0
  386. package/dist/web/service.js +207 -0
  387. package/dist/web/ssrf-guard.js +44 -0
  388. package/dist/web/tool.js +76 -0
  389. package/dist/web/types.js +10 -0
  390. package/dist/webui/assets/index-B-t2O8gh.js +22 -0
  391. package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
  392. package/dist/webui/index.html +14 -0
  393. package/dist/webui/index.js +14 -0
  394. package/dist/webui/server.js +549 -0
  395. package/dist/webui/types.js +38 -0
  396. package/examples/README.md +26 -0
  397. package/examples/minimal-code-plugin/entry.js +43 -0
  398. package/examples/minimal-code-plugin/package.json +12 -0
  399. package/examples/pure-skill-pack/package.json +14 -0
  400. package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
  401. package/package.json +86 -0
@@ -0,0 +1,131 @@
1
+ import { MEMORY_CITE_RE } from './inject.js';
2
+ import { MEMORY_LAST_ASSISTANT_TEXT_LRU } from './types.js';
3
+ /* ---------------- 文本面提取 ---------------- */
4
+ /**
5
+ * assistant/message durable data 文本提取(wiring 落 data.content =
6
+ * (text|thinking 块)[]——toolCall 块不内联;引用只发生在 text 块呈现面,
7
+ * thinking 块天然排除)。坏形返回 null(消费侧静默跳过——尽力而为)。
8
+ * 导出面(§4 回看缓存共用——「紧邻前一条 assistant 文本」的取数单源)。
9
+ */
10
+ export function assistantTextOf(data) {
11
+ if (typeof data !== 'object' || data === null)
12
+ return null;
13
+ const content = data.content;
14
+ if (typeof content === 'string')
15
+ return content;
16
+ if (!Array.isArray(content))
17
+ return null;
18
+ return content
19
+ .filter((b) => {
20
+ const block = b;
21
+ return typeof block === 'object' && block !== null && block.type === 'text' && typeof block.text === 'string';
22
+ })
23
+ .map((b) => b.text)
24
+ .join('\n');
25
+ }
26
+ /* ---------------- 引用解析 ---------------- */
27
+ /**
28
+ * 文本面引用短 id 解析(同消息同短 id 去重——一条消息对一条记忆计一次;
29
+ * 首现序保序)。/g 正则共享——进入与离开双复位(scan.ts 无状态纪律同律)。
30
+ */
31
+ export function parseCitations(text) {
32
+ MEMORY_CITE_RE.lastIndex = 0;
33
+ const shortIds = [];
34
+ const seen = new Set();
35
+ let m;
36
+ while ((m = MEMORY_CITE_RE.exec(text)) !== null) {
37
+ const short = m[1];
38
+ if (!seen.has(short)) {
39
+ seen.add(short);
40
+ shortIds.push(short);
41
+ }
42
+ }
43
+ MEMORY_CITE_RE.lastIndex = 0;
44
+ return shortIds;
45
+ }
46
+ /** 建引用回写挂件(装配面挂会话事件流——消费 idiom 同 18c-5 周期路) */
47
+ export function createCiteRecorder(deps) {
48
+ const warn = deps.warn ?? (() => { });
49
+ return {
50
+ onEvent(sessionId, type, data) {
51
+ if (type !== 'assistant/message')
52
+ return;
53
+ try {
54
+ const text = assistantTextOf(data);
55
+ if (text === null || text === '')
56
+ return;
57
+ const shorts = parseCitations(text);
58
+ if (shorts.length === 0)
59
+ return;
60
+ // 前缀归责三态:零命中不进 shorts;多命中 = 歧义全部忽略;恰一命中 = 唯一归属
61
+ const fullIds = [];
62
+ for (const short of shorts) {
63
+ const matches = deps.dao.resolveShortId(short);
64
+ if (matches.length === 1)
65
+ fullIds.push(matches[0]);
66
+ }
67
+ if (fullIds.length > 0)
68
+ deps.dao.markUsed(fullIds, sessionId);
69
+ }
70
+ catch (err) {
71
+ warn(`[memory] 引用回写尽力而为止步:${err instanceof Error ? err.message : String(err)}`);
72
+ }
73
+ },
74
+ };
75
+ }
76
+ /**
77
+ * 纠正负效用回写(§4 即时路第二动作——§6 解析面**全复用**):对被纠正回答的
78
+ * 文本面解析引用标记(MEMORY_CITE_RE + 同消息同短 id 去重 = 守卫④「同事件
79
+ * 一次」的物理承载)→ 前缀归责三态(与 cite 正账同律)→ dao.markCorrected
80
+ * 批量(corrected_count+1 + op='corrected-cite' 流水带**纠正发生会话**键——
81
+ * 守卫①③归 dao 语句本体)。尽力而为:全程 try/catch warn(计量面写点不
82
+ * 反噬提取主路——两动作失败路径分立的回写侧保障)。
83
+ */
84
+ export function recordCorrectedCites(dao, sessionId, assistantText, warn = () => { }) {
85
+ try {
86
+ const shorts = parseCitations(assistantText);
87
+ if (shorts.length === 0)
88
+ return;
89
+ const fullIds = [];
90
+ for (const short of shorts) {
91
+ const matches = dao.resolveShortId(short);
92
+ if (matches.length === 1)
93
+ fullIds.push(matches[0]);
94
+ }
95
+ if (fullIds.length > 0)
96
+ dao.markCorrected(fullIds, sessionId);
97
+ }
98
+ catch (err) {
99
+ warn(`[memory] 纠正负效用回写尽力而为止步:${err instanceof Error ? err.message : String(err)}`);
100
+ }
101
+ }
102
+ /**
103
+ * per-session 最近 assistant 文本回看缓存(§4 回看位实现——装配面同一
104
+ * session/event 消费点内喂入,零新事件通道零宿主改动;LRU 帽族同 §6 epochs)。
105
+ * **紧邻前一条语义忠实**:每次 assistant/message 到达即覆写该会话条目——
106
+ * 空文本面(纯 toolCall/thinking 消息)同覆写为 '',回看自然空手(跨条回看
107
+ * 无判据不发明)。读即触位(紧随其后的纠正回看高频——保热会话不被挤)。
108
+ */
109
+ export function createLastAssistantTextCache(capacity = MEMORY_LAST_ASSISTANT_TEXT_LRU) {
110
+ const cache = new Map();
111
+ return {
112
+ observe(sessionId, data) {
113
+ const text = assistantTextOf(data) ?? '';
114
+ cache.delete(sessionId); // 先删再插 = 触位到 Map 尾(最近)
115
+ cache.set(sessionId, text);
116
+ if (cache.size > capacity) {
117
+ const oldest = cache.keys().next().value;
118
+ if (oldest !== undefined)
119
+ cache.delete(oldest);
120
+ }
121
+ },
122
+ get(sessionId) {
123
+ const text = cache.get(sessionId);
124
+ if (text === undefined)
125
+ return null;
126
+ cache.delete(sessionId);
127
+ cache.set(sessionId, text);
128
+ return text;
129
+ },
130
+ };
131
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * memory 域错误码注册(06 §8.1/§3/§5/§7——MEMORY_ 前缀族;02 §5.3 批 18c-1
3
+ * 明列两码 + 批 18c-2 工具面扩三码,本仓新立)。
4
+ *
5
+ * 码语义分层:MEMORY_SECRET_DETECTED 管写前 secret 扫描命中拒写(DAO 入库
6
+ * 单点执法——四写点物理汇入同一入库面,没有绕过扫描的写入方;诊断 log-only
7
+ * 不回写疑似密钥本体);MEMORY_ENTRY_INVALID 管候选/写请求坏形拒(闭集/
8
+ * 形状/越界判据——18c-2 扩 promotedToSkill 词法与 ttl days 形两判据);
9
+ * MEMORY_NOT_FOUND / MEMORY_FROZEN / MEMORY_REVISION_NOT_FOUND 管持有面
10
+ * 工具动词守卫(作用行缺席 / 撞冻结行 / 版本缺席)。本文件由模块公开面
11
+ * index.ts 引入(注册纪律:import 发生才注册)。
12
+ */
13
+ import { registerErrorCodes } from '../contracts/index.js';
14
+ registerErrorCodes([
15
+ {
16
+ code: 'MEMORY_SECRET_DETECTED',
17
+ module: 'memory',
18
+ description: '写前 secret 扫描命中拒写——DAO 入库单点执法(即时路/周期路/memory_write/导入直插四写点汇入同一入库面);诊断 log-only 不回写疑似密钥本体(报 pattern 名,不落命中文本)',
19
+ },
20
+ {
21
+ code: 'MEMORY_ENTRY_INVALID',
22
+ module: 'memory',
23
+ description: '候选/写请求坏形拒——kind 非七值闭集、owner_key 形违例(global | project:<根路径哈希> 两形外)、confidence 越界 [0,1]、summary/content 空、source_refs 元素坏形、promotedToSkill 技能名词法违例(^[a-z0-9]+(?:-[a-z0-9]+)*$ 且 ≤64)、ttl days 形违例(非正整数且非 null)',
24
+ },
25
+ {
26
+ code: 'MEMORY_NOT_FOUND',
27
+ module: 'memory',
28
+ description: '工具动词作用行缺席——forget/restore/freeze/unfreeze/ttl 携 id 在库缺席响亮拒(GOAL_NOT_FOUND 同构守卫)',
29
+ },
30
+ {
31
+ code: 'MEMORY_FROZEN',
32
+ module: 'memory',
33
+ description: '持有面写动词撞冻结行拒——forget(含 promotedToSkill 搬家)/ttl/restore 带 revision 内容回滚腿;frozen 免覆写执法,解冻-再写唯一路径',
34
+ },
35
+ {
36
+ code: 'MEMORY_REVISION_NOT_FOUND',
37
+ module: 'memory',
38
+ description: 'restore 带 revision 而版本缺席——无链条目带版本拒、revision 越界同码(带版本 ⊃ 状态复活)',
39
+ },
40
+ {
41
+ code: 'MEMORY_EXPORT_ROOT_DENIED',
42
+ module: 'memory',
43
+ description: '导出落盘路径越界可写根拒(批 18c-8)——/memory-export 命令 handler 内显式 isInsideRoot 判定(守门管道对插件内文件写不可见、memory 件无 safety 拓扑边;同律先例 SKILLS_WRITE_ROOT_DENIED / FS_OUTSIDE_WRITABLE_ROOTS——越界拒、边界分隔符守卫同款)',
44
+ },
45
+ {
46
+ code: 'MEMORY_IMPORT_FORMAT_INVALID',
47
+ module: 'memory',
48
+ description: '导入文件首行 header 坏形整文件拒(批 18c-8)——magic 串 berry-agent-memory 不符或 formatVersion ≠ 1;行级坏形不入此码(行级宽容分账 rejectedMalformed——恢复式运维动词不弃批)',
49
+ },
50
+ ]);
@@ -0,0 +1,71 @@
1
+ /**
2
+ * /memory-export · /memory-import 命令处理器(批 18c-8——06 §3/§7:用户
3
+ * 命令面非模型工具面——操作者运维动词〔备份/迁移〕,模型可发起的全量明文
4
+ * 记忆落盘 = 可诱导批量外泄路径,故不入工具面)。
5
+ *
6
+ * 形态律同 /goal /rewind /tick(命令件先例):argv → 人读文本;服务面守卫错
7
+ * (BaseError)折文本不抛——命令面是用户面不是异常面。TUI 命令注册归批 12
8
+ * host 装配批(corePlugins 注册表现空)。
9
+ *
10
+ * 可写根判定:导出落盘路径越界拒 MEMORY_EXPORT_ROOT_DENIED(装配闭包注入
11
+ * 可写根列表 + handler 内显式 isWithinRoots 判定——06 §3:守门管道对插件内
12
+ * 文件写不可见、memory 件无 safety 拓扑边);**导入读面无根判定**(读取任意
13
+ * 路径是用户显式动词,写入面才受可写根约束——同 skills loadFace 读面先例)。
14
+ */
15
+ import { readFileSync, writeFileSync } from 'node:fs';
16
+ import { BaseError } from '../contracts/index.js';
17
+ import { buildMemoryExport, isWithinRoots, runMemoryImport } from './port.js';
18
+ export const MEMORY_EXPORT_USAGE = '用法:/memory-export <path> [owner] —— 记忆导出为 JSONL 明文文件(owner 省略 = 全 owner;路径须在可写根内)';
19
+ export const MEMORY_IMPORT_USAGE = '用法:/memory-import <path> —— 从 JSONL 文件恢复式导入(按 id 幂等:已有条目跳过、零合并零覆写)';
20
+ /** /memory-export 处理器(argv = [path, owner?] → 人读文本;守卫错折文本) */
21
+ export async function runMemoryExportCommand(argv, deps) {
22
+ const [target, owner] = argv;
23
+ try {
24
+ if (!target)
25
+ return `缺路径。\n${MEMORY_EXPORT_USAGE}`;
26
+ // 落盘路径判定(越界拒——isWithinRoots 同律复用引证〔skills 先例函数体拷贝 + 注记互证〕)
27
+ if (!isWithinRoots(target, deps.writableRoots())) {
28
+ throw new BaseError('MEMORY_EXPORT_ROOT_DENIED', `导出落盘路径不在可写根内:${target}`);
29
+ }
30
+ const text = buildMemoryExport(deps.dao, {
31
+ ownerKey: owner,
32
+ ownerRoots: deps.ownerRoots(),
33
+ now: deps.now(),
34
+ });
35
+ writeFileSync(target, text, 'utf8');
36
+ const count = text.trimEnd().split('\n').length - 1; // 首行 header 外即数据行数
37
+ const scope = owner === undefined ? '全部 owner' : `owner ${owner}`;
38
+ return [
39
+ `已导出 ${count} 条记忆(${scope})→ ${target}`,
40
+ '警示:导出文件含全部记忆明文——按敏感数据保管、勿提交进仓库。',
41
+ ].join('\n');
42
+ }
43
+ catch (err) {
44
+ // 守卫错折文本(命令面是用户面——BaseError 码与人读原因直呈)
45
+ if (err instanceof BaseError)
46
+ return `${err.code}:${err.message}`;
47
+ throw err;
48
+ }
49
+ }
50
+ /** /memory-import 处理器(argv = [path] → 人读文本;header 坏形整文件拒折文本) */
51
+ export async function runMemoryImportCommand(argv, deps) {
52
+ const [target] = argv;
53
+ try {
54
+ if (!target)
55
+ return `缺路径。\n${MEMORY_IMPORT_USAGE}`;
56
+ const text = readFileSync(target, 'utf8');
57
+ const r = runMemoryImport(text, deps.dao);
58
+ if (r.inserted + r.skippedExisting + r.rejectedSecret + r.rejectedMalformed === 0) {
59
+ return '导入完成:文件内无数据行(仅 header)。';
60
+ }
61
+ return [
62
+ `导入完成:新插 ${r.inserted} 条 · 已在跳过 ${r.skippedExisting} 条`,
63
+ `    secret 拒写 ${r.rejectedSecret} 条 · 坏形跳过 ${r.rejectedMalformed} 条(恢复式幂等——零合并零覆写)`,
64
+ ].join('\n');
65
+ }
66
+ catch (err) {
67
+ if (err instanceof BaseError)
68
+ return `${err.code}:${err.message}`;
69
+ throw err;
70
+ }
71
+ }
@@ -0,0 +1,219 @@
1
+ import { tokenizeForMerge, utilityScore } from './merge.js';
2
+ import { parseJsonPayload } from './review.js';
3
+ import { MEMORY_CONSOLIDATION_ANCHOR_MS, MEMORY_CONSOLIDATION_STALE_DAYS, MEMORY_DECAY_FACTOR, MEMORY_DAY_MS, MEMORY_OWNER_CAPACITY, llmTextOf, } from './types.js';
4
+ import { Type } from 'typebox';
5
+ import { Value } from 'typebox/value';
6
+ /* ---------------- LLM 计划 schema(TypeBox 深校验——未知字段拒收) ---------------- */
7
+ const MERGE_SUGGESTION = Type.Object({
8
+ action: Type.Literal('merge'),
9
+ keep: Type.String(),
10
+ drop: Type.String(),
11
+ reason: Type.String(),
12
+ }, { additionalProperties: false });
13
+ const DECAY_SUGGESTION = Type.Object({
14
+ action: Type.Literal('decay'),
15
+ id: Type.String(),
16
+ reason: Type.String(),
17
+ }, { additionalProperties: false });
18
+ const PLAN = Type.Object({
19
+ merges: Type.Array(MERGE_SUGGESTION),
20
+ decays: Type.Array(DECAY_SUGGESTION),
21
+ }, { additionalProperties: false });
22
+ /* ---------------- 工厂 ---------------- */
23
+ /** 空结果便捷构造 */
24
+ function result(outcome, extra = {}) {
25
+ return { outcome, candidateCount: 0, merged: 0, decayed: 0, rejectedGroups: 0, ignoredSuggestions: 0, ...extra };
26
+ }
27
+ /** 整理拍系统提示词(只出 JSON 对象——护栏在执行腿) */
28
+ const CONSOLIDATE_SYSTEM_PROMPT = [
29
+ '你是记忆整理器:对候选记忆条目给出深层语义合并与降权建议。',
30
+ '只输出一个 JSON 对象(无其他文本),形如',
31
+ '{"merges":[{"action":"merge","keep":"保留条目id","drop":"被并条目id","reason":"合并理由"}],"decays":[{"action":"decay","id":"条目id","reason":"降权理由"}]}',
32
+ '规则:只建议语义重复或互为矛盾面的合并(keep 承载合并后语义);只对确证过时或低质条目建议降权;',
33
+ 'reason 必须引用条目摘要中出现的关键词;无建议时输出 {"merges":[],"decays":[]}。',
34
+ ].join('\n');
35
+ /** 建整理器(进程单例装配——实例态 = 水位基线 + anchor 时钟) */
36
+ export function createConsolidator(deps) {
37
+ const { dao, llm } = deps;
38
+ const warn = deps.warn ?? (() => { });
39
+ const capacity = deps.capacity ?? MEMORY_OWNER_CAPACITY;
40
+ const staleDays = deps.staleDays ?? MEMORY_CONSOLIDATION_STALE_DAYS;
41
+ const decayFactor = deps.decayFactor ?? MEMORY_DECAY_FACTOR;
42
+ const anchorMs = deps.anchorMs ?? MEMORY_CONSOLIDATION_ANCHOR_MS;
43
+ const now = deps.now ?? Date.now;
44
+ // —— 实例内存态(落码定形注:重启视为有新摄入——首轮恒不水位短路)
45
+ let lastRunClock = null;
46
+ let baselineIntakeAt = null;
47
+ /** 候选集构造(三源并集去重;frozen 显式排——listVisible 谓词面已排 expired/终态) */
48
+ function buildCandidates(visible, nowMs, pollutedSessions) {
49
+ const candidates = new Map();
50
+ const add = (row) => {
51
+ if (row.frozen)
52
+ return; // frozen 免整理全档
53
+ candidates.set(row.id, row);
54
+ };
55
+ // 源一:老化(updated_at 超 staleDays)
56
+ const staleBefore = nowMs - staleDays * MEMORY_DAY_MS;
57
+ for (const row of visible) {
58
+ if (row.updatedAt < staleBefore)
59
+ add(row);
60
+ }
61
+ // 源二:容量溢出(owner 分组——效用综合分升序取最低分盈余差额行;frozen 不计容量)
62
+ const byOwner = new Map();
63
+ for (const row of visible) {
64
+ if (row.frozen)
65
+ continue;
66
+ const group = byOwner.get(row.ownerKey) ?? [];
67
+ group.push(row);
68
+ byOwner.set(row.ownerKey, group);
69
+ }
70
+ for (const group of byOwner.values()) {
71
+ if (group.length <= capacity)
72
+ continue;
73
+ const surplus = group.length - capacity;
74
+ const ranked = [...group].sort((a, b) => utilityScore(a) - utilityScore(b)); // 升序——低分先入
75
+ for (const row of ranked.slice(0, surplus))
76
+ add(row);
77
+ }
78
+ // 源三:polluted 批(§4.1——refs 命中 polluted 会话的条目进淘汰候选)
79
+ if (pollutedSessions.size > 0) {
80
+ for (const row of visible) {
81
+ if (row.sourceRefs.some((ref) => pollutedSessions.has(ref.sessionId)))
82
+ add(row);
83
+ }
84
+ }
85
+ return candidates;
86
+ }
87
+ /** 理由护栏:reason 分词与摘要 token 交集 ≥1(零交集/空分词 → false 整组驳回) */
88
+ function reasonSupported(reason, summaries) {
89
+ const reasonTokens = new Set(tokenizeForMerge(reason));
90
+ if (reasonTokens.size === 0)
91
+ return false;
92
+ for (const summary of summaries) {
93
+ for (const token of tokenizeForMerge(summary)) {
94
+ if (reasonTokens.has(token))
95
+ return true;
96
+ }
97
+ }
98
+ return false;
99
+ }
100
+ /** 存活重验(执行前逐条复核——missing/终态/frozen 均败) */
101
+ function alive(id) {
102
+ const row = dao.get(id);
103
+ return row !== undefined && row.status === 'active' && !row.frozen;
104
+ }
105
+ /** 候选清单行(LLM 可见面——id/种类/摘要/三维分原料/未变更天数) */
106
+ function candidateLine(row, nowMs) {
107
+ const ageDays = Math.floor((nowMs - row.updatedAt) / MEMORY_DAY_MS);
108
+ return `[${row.id}] kind=${row.kind} summary=${row.summary}(confidence=${row.confidence} evidence=${row.evidenceCount} usage=${row.usageCount} ${ageDays}d 未变更)`;
109
+ }
110
+ return {
111
+ async run(input) {
112
+ const nowMs = now();
113
+ // —— 护栏一(anchor 腿):拍间最小间隔——只对真实拍(ran/empty)进位
114
+ if (lastRunClock !== null && nowMs - lastRunClock < anchorMs)
115
+ return result('skipped-anchor');
116
+ const visible = dao.listVisible();
117
+ // —— 护栏一(水位腿):无新摄入(基线 = 拍终时钟——absorb 自身写不构成摄入)
118
+ const currentMax = visible.reduce((m, r) => Math.max(m, r.updatedAt), 0);
119
+ if (baselineIntakeAt !== null && currentMax <= baselineIntakeAt)
120
+ return result('skipped-watermark');
121
+ const polluted = new Set(input?.pollutedSessions ?? []);
122
+ const candidates = buildCandidates(visible, nowMs, polluted);
123
+ if (candidates.size === 0) {
124
+ // 候选面已稳定:烧 anchor + 进位基线(防同摄入面空转循环)
125
+ lastRunClock = nowMs;
126
+ baselineIntakeAt = Math.max(baselineIntakeAt ?? 0, currentMax, nowMs);
127
+ return result('skipped-empty');
128
+ }
129
+ // —— 护栏一(预算腿):canAfford 拒跳过本轮(不烧 anchor——预算恢复即拍)
130
+ if (!llm.canAfford('background'))
131
+ return result('skipped-budget');
132
+ // —— LLM 拍
133
+ let plan;
134
+ try {
135
+ const completion = await llm.complete({
136
+ systemPrompt: CONSOLIDATE_SYSTEM_PROMPT,
137
+ messages: [
138
+ {
139
+ role: 'user',
140
+ content: `候选记忆条目:\n${[...candidates.values()].map((r) => candidateLine(r, nowMs)).join('\n')}`,
141
+ },
142
+ ],
143
+ priority: 'background',
144
+ });
145
+ const parsed = parseJsonPayload(llmTextOf(completion.message.content));
146
+ plan = parsed !== null && Value.Check(PLAN, parsed) ? parsed : null;
147
+ }
148
+ catch (error) {
149
+ // complete 抛(网络/预算竞态)——跳过本轮,状态零进位
150
+ warn(`memory consolidation 拍失败(跳过本轮):${error instanceof Error ? error.message : String(error)}`);
151
+ return result('skipped-error');
152
+ }
153
+ if (plan === null)
154
+ return result('skipped-parse'); // 计划整体坏形——下周期重试
155
+ // —— 执行腿(护栏逐条执法;物理动作全走 DAO 既有路径)
156
+ let merged = 0;
157
+ let decayed = 0;
158
+ let rejectedGroups = 0;
159
+ let ignoredSuggestions = 0;
160
+ for (const m of plan.merges) {
161
+ const keep = candidates.get(m.keep);
162
+ const drop = candidates.get(m.drop);
163
+ // 幻觉护栏:集外 id 一律忽略
164
+ if (!keep || !drop) {
165
+ ignoredSuggestions++;
166
+ continue;
167
+ }
168
+ // 理由护栏:零交集整组驳回
169
+ if (!reasonSupported(m.reason, [keep.summary, drop.summary])) {
170
+ rejectedGroups++;
171
+ continue;
172
+ }
173
+ // 存活重验(拍间失活/冻结)
174
+ if (!alive(m.keep) || !alive(m.drop)) {
175
+ ignoredSuggestions++;
176
+ continue;
177
+ }
178
+ try {
179
+ // 血缘继承 + drop 终态 llm:<keep> 内联;批 ev-1 reason 入链——护栏
180
+ // 校验后的 LLM 建议组 reason 随版本行持久化(用完即弃 → durable 因由)
181
+ dao.absorb(m.keep, m.drop, m.reason);
182
+ merged++;
183
+ }
184
+ catch {
185
+ ignoredSuggestions++; // 执行期拒(并发终态等)——吞不反噬
186
+ }
187
+ }
188
+ for (const d of plan.decays) {
189
+ const target = candidates.get(d.id);
190
+ if (!target) {
191
+ ignoredSuggestions++;
192
+ continue;
193
+ }
194
+ if (!reasonSupported(d.reason, [target.summary])) {
195
+ rejectedGroups++;
196
+ continue;
197
+ }
198
+ if (!alive(d.id)) {
199
+ ignoredSuggestions++;
200
+ continue;
201
+ }
202
+ try {
203
+ // confidence × factor + 版本 cause='decay';批 ev-1——decay 判据描述
204
+ // (护栏校验后的 LLM 自由文本 reason)随版本行持久化
205
+ dao.decay(d.id, decayFactor, d.reason);
206
+ decayed++;
207
+ }
208
+ catch {
209
+ ignoredSuggestions++;
210
+ }
211
+ }
212
+ // —— 拍终状态进位:anchor + 水位基线 = 拍终时钟(自身 absorb 写 ≤ 基线)
213
+ const settleClock = now();
214
+ lastRunClock = settleClock;
215
+ baselineIntakeAt = Math.max(baselineIntakeAt ?? 0, settleClock);
216
+ return result('ran', { candidateCount: candidates.size, merged, decayed, rejectedGroups, ignoredSuggestions });
217
+ },
218
+ };
219
+ }
@@ -0,0 +1,109 @@
1
+ import { createConsolidator } from './consolidate.js';
2
+ import { createPollutionTracker } from './pollution.js';
3
+ import { runMemoryReview } from './review.js';
4
+ import { MEMORY_REVIEW_TOOL_CALL_THRESHOLD, MEMORY_REVIEW_TURN_THRESHOLD, MEMORY_REVIEW_WINDOW_TURNS, } from './types.js';
5
+ /* ---------------- 审阅窗切片(纯函数) ---------------- */
6
+ /**
7
+ * 最近 N 个回合的事件切片(定形注——审阅窗):自尾向前跳过最近 N 个回合的
8
+ * turn/end,边界取第 N+1 个 turn/end **之后**——窗口恰含最近 N 个回合(回合
9
+ * 事件 = 上一收尾之后至本收尾);回合不足窗全量返回。
10
+ */
11
+ export function sliceReviewWindow(events, windowTurns = MEMORY_REVIEW_WINDOW_TURNS) {
12
+ let seen = 0;
13
+ for (let i = events.length - 1; i >= 0; i--) {
14
+ if (events[i].type === 'turn/end') {
15
+ seen++;
16
+ if (seen === windowTurns + 1)
17
+ return events.slice(i + 1); // 第 N+1 个收尾之后——最近 N 回合全量
18
+ }
19
+ }
20
+ return events; // 回合不足窗——全量
21
+ }
22
+ /* ---------------- 工厂 ---------------- */
23
+ /** 建周期路编排件(进程单例装配) */
24
+ export function createMemoryCycle(deps) {
25
+ const warn = deps.warn ?? (() => { });
26
+ const pollution = deps.pollution ?? createPollutionTracker();
27
+ const consolidator = deps.consolidator ??
28
+ createConsolidator({ dao: deps.dao, llm: deps.llm, ...(deps.warn !== undefined ? { warn: deps.warn } : {}) });
29
+ const turnThreshold = deps.thresholds?.turns ?? MEMORY_REVIEW_TURN_THRESHOLD;
30
+ const toolCallThreshold = deps.thresholds?.toolCalls ?? MEMORY_REVIEW_TOOL_CALL_THRESHOLD;
31
+ const windowTurns = deps.thresholds?.windowTurns ?? MEMORY_REVIEW_WINDOW_TURNS;
32
+ // 会话计数器(里程表)+ due 集 + inFlight 单飞锁——全进程内存态
33
+ const counters = new Map();
34
+ const due = new Set();
35
+ const inFlight = new Set();
36
+ return {
37
+ pollution,
38
+ consolidator,
39
+ onDurableEvent(sessionId, event) {
40
+ if (event.type === 'turn/end') {
41
+ const c = counters.get(sessionId) ?? { turns: 0, toolCalls: 0 };
42
+ c.turns++;
43
+ counters.set(sessionId, c);
44
+ }
45
+ else if (event.type === 'tool/call') {
46
+ const c = counters.get(sessionId) ?? { turns: 0, toolCalls: 0 };
47
+ c.toolCalls++;
48
+ counters.set(sessionId, c);
49
+ // 污染标记位(§4.1——载荷携 name;坏形载荷无 name 不标记)
50
+ const data = event.data;
51
+ if (data !== null && typeof data === 'object' && typeof data.name === 'string') {
52
+ pollution.markIfPolluted(sessionId, data.name);
53
+ }
54
+ }
55
+ else {
56
+ return; // 其余事件与计数无关
57
+ }
58
+ const c = counters.get(sessionId);
59
+ if (c.turns >= turnThreshold || c.toolCalls >= toolCallThreshold)
60
+ due.add(sessionId);
61
+ },
62
+ dueSessions() {
63
+ return [...due];
64
+ },
65
+ async fire(sessionId) {
66
+ // inFlight 单飞(定形注:inFlight 跳过——计数器照走里程表)
67
+ if (inFlight.has(sessionId)) {
68
+ return { outcome: 'skipped-inflight', sweptExpired: 0 };
69
+ }
70
+ inFlight.add(sessionId);
71
+ try {
72
+ // —— fire 首步:sweepExpired 同步物化(TTL 清扫 + 访问日志窗口清扫同拍
73
+ // 单事务——polluted 也不例外;周期报告面取 .expired 计数)
74
+ const sweptExpired = deps.dao.sweepExpired().expired;
75
+ // —— 资格检查(§4.1 两路入口同一检查——polluted 跳过 review;遗忘走
76
+ // consolidation 淘汰批:polluted 会话集随轮注入)
77
+ const pollutedNow = pollution.isPolluted(sessionId);
78
+ let review;
79
+ if (!pollutedNow) {
80
+ const window = sliceReviewWindow(deps.fetchEvents(sessionId), windowTurns);
81
+ review = await runMemoryReview({ dao: deps.dao, llm: deps.llm, warn }, sessionId, window);
82
+ }
83
+ // —— consolidation 拍(polluted 会话集注入——§4.1 淘汰批圈候选)
84
+ const consolidation = await consolidator.run({ pollutedSessions: pollution.pollutedSessions() });
85
+ // —— 计数器复位(里程表——review 完成后归零;due 同清)
86
+ counters.delete(sessionId);
87
+ due.delete(sessionId);
88
+ return {
89
+ outcome: pollutedNow ? 'skipped-polluted' : 'reviewed',
90
+ sweptExpired,
91
+ ...(review !== undefined ? { review } : {}),
92
+ consolidation,
93
+ };
94
+ }
95
+ catch (error) {
96
+ // 尽力而为(06 §4——周期路失败不重试不反噬;进程日志是唯一观测面)
97
+ warn(`memory 周期路 fire 失败(尽力而为跳过):${error instanceof Error ? error.message : String(error)}`);
98
+ return { outcome: pollutedNowRef(pollution, sessionId), sweptExpired: 0 };
99
+ }
100
+ finally {
101
+ inFlight.delete(sessionId);
102
+ }
103
+ },
104
+ };
105
+ }
106
+ /** catch 面资格回查(review 未跑——按现行资格态归类) */
107
+ function pollutedNowRef(pollution, sessionId) {
108
+ return pollution.isPolluted(sessionId) ? 'skipped-polluted' : 'reviewed';
109
+ }