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.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/.api-emit.stamp +1 -0
- package/dist/.build-meta.json +6 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/loop.js +139 -0
- package/dist/agent/queue.js +119 -0
- package/dist/agent/stream.js +62 -0
- package/dist/agent/tools-batch.js +221 -0
- package/dist/agent/types.js +8 -0
- package/dist/api/berry-agent-llm.d.ts +18 -0
- package/dist/api/berry-agent.d.ts +9 -0
- package/dist/api/surface.json +1632 -0
- package/dist/api/tsconfig.paths.json +12 -0
- package/dist/api/typebox-compile.d.ts +2 -0
- package/dist/api/typebox-value.d.ts +2 -0
- package/dist/api/typebox.d.ts +2 -0
- package/dist/browser/cdp.js +256 -0
- package/dist/browser/codes.js +31 -0
- package/dist/browser/discover.js +107 -0
- package/dist/browser/engine.js +169 -0
- package/dist/browser/index.js +17 -0
- package/dist/browser/install.js +342 -0
- package/dist/browser/page.js +321 -0
- package/dist/browser/service.js +158 -0
- package/dist/browser/tools.js +198 -0
- package/dist/browser/types.js +81 -0
- package/dist/channels/ask-queue.js +82 -0
- package/dist/channels/commands.js +129 -0
- package/dist/channels/engine/cell.js +143 -0
- package/dist/channels/engine/diff.js +143 -0
- package/dist/channels/engine/engine.js +416 -0
- package/dist/channels/engine/index.js +8 -0
- package/dist/channels/engine/input-keys.js +86 -0
- package/dist/channels/engine/input.js +596 -0
- package/dist/channels/engine/memory-io.js +71 -0
- package/dist/channels/engine/process-io.js +38 -0
- package/dist/channels/engine/types.js +17 -0
- package/dist/channels/engine/width.js +122 -0
- package/dist/channels/index.js +15 -0
- package/dist/channels/registry.js +56 -0
- package/dist/channels/sdk/admit.js +29 -0
- package/dist/channels/sdk/backend.js +102 -0
- package/dist/channels/sdk/cursor.js +56 -0
- package/dist/channels/sdk/index.js +20 -0
- package/dist/channels/sdk/jsonl.js +208 -0
- package/dist/channels/sdk/protocol.js +24 -0
- package/dist/channels/sdk/schema.js +91 -0
- package/dist/channels/sdk/wire-core.js +446 -0
- package/dist/channels/service.js +169 -0
- package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
- package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
- package/dist/channels/tui/autocomplete/popup.js +148 -0
- package/dist/channels/tui/autocomplete/provider.js +7 -0
- package/dist/channels/tui/autocomplete/token.js +68 -0
- package/dist/channels/tui/backend/ansi-rows.js +211 -0
- package/dist/channels/tui/backend/main-screen.js +207 -0
- package/dist/channels/tui/backend/osc.js +91 -0
- package/dist/channels/tui/backend/transcript.js +230 -0
- package/dist/channels/tui/backend/tui-backend.js +915 -0
- package/dist/channels/tui/editor/editor-model.js +543 -0
- package/dist/channels/tui/editor/editor-view.js +140 -0
- package/dist/channels/tui/editor/editor.js +275 -0
- package/dist/channels/tui/editor/undo-stack.js +35 -0
- package/dist/channels/tui/editor/visual-lines.js +126 -0
- package/dist/channels/tui/editor/word-nav.js +106 -0
- package/dist/channels/tui/history/history-viewer.js +403 -0
- package/dist/channels/tui/index.js +3 -0
- package/dist/channels/tui/layout.js +167 -0
- package/dist/channels/tui/markdown/blocks.js +111 -0
- package/dist/channels/tui/markdown/inline.js +75 -0
- package/dist/channels/tui/markdown/markdown.js +182 -0
- package/dist/channels/tui/memory/memory-viewer.js +540 -0
- package/dist/channels/tui/overlay/alt-screen.js +105 -0
- package/dist/channels/tui/overlay/overlay.js +57 -0
- package/dist/channels/tui/overlay/select-confirm.js +156 -0
- package/dist/channels/tui/panels/todo-panel.js +54 -0
- package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
- package/dist/channels/tui/scroll/scroll-view.js +254 -0
- package/dist/channels/tui/status/status-line.js +76 -0
- package/dist/channels/tui/text.js +38 -0
- package/dist/channels/tui/theme.js +41 -0
- package/dist/channels/types.js +11 -0
- package/dist/channels/ui-core.js +209 -0
- package/dist/checkpoint/capture.js +45 -0
- package/dist/checkpoint/codes.js +33 -0
- package/dist/checkpoint/command.js +82 -0
- package/dist/checkpoint/gate.js +48 -0
- package/dist/checkpoint/index.js +20 -0
- package/dist/checkpoint/restore.js +172 -0
- package/dist/checkpoint/store.js +247 -0
- package/dist/checkpoint/types.js +4 -0
- package/dist/checkpoint/walk.js +146 -0
- package/dist/compaction/ccr-tools.js +112 -0
- package/dist/compaction/ccr.js +84 -0
- package/dist/compaction/codes.js +21 -0
- package/dist/compaction/index.js +18 -0
- package/dist/compaction/policy.js +200 -0
- package/dist/compaction/service.js +422 -0
- package/dist/compaction/slots.js +121 -0
- package/dist/compaction/types.js +10 -0
- package/dist/context/codes.js +40 -0
- package/dist/context/events.js +160 -0
- package/dist/context/index.js +13 -0
- package/dist/context/logger.js +136 -0
- package/dist/context/scope.js +182 -0
- package/dist/context/workspace.js +104 -0
- package/dist/contracts/agent-events.d.ts +73 -0
- package/dist/contracts/agent-events.js +16 -0
- package/dist/contracts/api.d.ts +249 -0
- package/dist/contracts/api.js +299 -0
- package/dist/contracts/approval.d.ts +37 -0
- package/dist/contracts/approval.js +9 -0
- package/dist/contracts/env-ref.d.ts +41 -0
- package/dist/contracts/env-ref.js +32 -0
- package/dist/contracts/errors.d.ts +56 -0
- package/dist/contracts/errors.js +266 -0
- package/dist/contracts/events.d.ts +70 -0
- package/dist/contracts/events.js +329 -0
- package/dist/contracts/index.d.ts +32 -0
- package/dist/contracts/index.js +26 -0
- package/dist/contracts/llm.d.ts +275 -0
- package/dist/contracts/llm.js +12 -0
- package/dist/contracts/messages.d.ts +63 -0
- package/dist/contracts/messages.js +65 -0
- package/dist/contracts/redact.d.ts +46 -0
- package/dist/contracts/redact.js +223 -0
- package/dist/contracts/tools.d.ts +197 -0
- package/dist/contracts/tools.js +33 -0
- package/dist/contracts/types.d.ts +302 -0
- package/dist/contracts/types.js +50 -0
- package/dist/contracts/ui.d.ts +122 -0
- package/dist/contracts/ui.js +15 -0
- package/dist/conversation/agent-service.js +61 -0
- package/dist/conversation/approval-wiring.js +82 -0
- package/dist/conversation/backoff.js +38 -0
- package/dist/conversation/codes.js +47 -0
- package/dist/conversation/control-tools.js +102 -0
- package/dist/conversation/control.js +210 -0
- package/dist/conversation/driver.js +922 -0
- package/dist/conversation/index.js +12 -0
- package/dist/conversation/model-visible.js +169 -0
- package/dist/conversation/open-tools.js +126 -0
- package/dist/conversation/reseed.js +132 -0
- package/dist/conversation/sessions.js +215 -0
- package/dist/conversation/todo.js +203 -0
- package/dist/conversation/types.js +35 -0
- package/dist/conversation/wiring.js +204 -0
- package/dist/credentials/codes.js +59 -0
- package/dist/credentials/commands.js +186 -0
- package/dist/credentials/env-ref.js +34 -0
- package/dist/credentials/index.js +28 -0
- package/dist/credentials/migration.js +18 -0
- package/dist/credentials/oauth.js +266 -0
- package/dist/credentials/refresh.js +152 -0
- package/dist/credentials/secrets.js +118 -0
- package/dist/credentials/types.js +30 -0
- package/dist/exec/bash.js +279 -0
- package/dist/exec/codes.js +37 -0
- package/dist/exec/env.js +99 -0
- package/dist/exec/environment.js +39 -0
- package/dist/exec/git-guard.js +623 -0
- package/dist/exec/index.js +24 -0
- package/dist/exec/registry.js +120 -0
- package/dist/exec/spawn.js +281 -0
- package/dist/exec/tail.js +77 -0
- package/dist/exec/types.js +5 -0
- package/dist/goal/codes.js +33 -0
- package/dist/goal/command.js +175 -0
- package/dist/goal/fold.js +121 -0
- package/dist/goal/gates.js +122 -0
- package/dist/goal/index.js +22 -0
- package/dist/goal/migration.js +48 -0
- package/dist/goal/service.js +501 -0
- package/dist/goal/todo-tool.js +174 -0
- package/dist/goal/types.js +1 -0
- package/dist/goal/update-tool.js +46 -0
- package/dist/host/approval-cmd.js +276 -0
- package/dist/host/assembly.js +1146 -0
- package/dist/host/boot-failures.js +104 -0
- package/dist/host/budget-advisory.js +74 -0
- package/dist/host/budget-broadcast.js +50 -0
- package/dist/host/builtins.js +54 -0
- package/dist/host/cli.js +544 -0
- package/dist/host/codes.js +120 -0
- package/dist/host/config-schema.js +195 -0
- package/dist/host/conversation-stack.js +654 -0
- package/dist/host/core-plugins.js +1685 -0
- package/dist/host/credentials-cmd.js +42 -0
- package/dist/host/disclosure.js +30 -0
- package/dist/host/dispatch.js +105 -0
- package/dist/host/doors-cmd.js +154 -0
- package/dist/host/dump-config.js +44 -0
- package/dist/host/hook-dispatch-guard.js +27 -0
- package/dist/host/import-gate.js +153 -0
- package/dist/host/index.js +54 -0
- package/dist/host/issue-session.js +241 -0
- package/dist/host/loader.js +372 -0
- package/dist/host/main.js +161 -0
- package/dist/host/manifest.js +381 -0
- package/dist/host/mcp-entry.js +53 -0
- package/dist/host/plugin-boot.js +952 -0
- package/dist/host/plugin-context.js +672 -0
- package/dist/host/plugin-install.js +480 -0
- package/dist/host/plugin-reload.js +124 -0
- package/dist/host/plugin-store.js +462 -0
- package/dist/host/plugin-tools.js +359 -0
- package/dist/host/plugin-uninstall.js +257 -0
- package/dist/host/plugins-cmd.js +302 -0
- package/dist/host/plugins-command.js +130 -0
- package/dist/host/plugins-config.js +195 -0
- package/dist/host/prompt-sections.js +133 -0
- package/dist/host/run-entry.js +544 -0
- package/dist/host/runtime.js +209 -0
- package/dist/host/scheduler-tick.js +294 -0
- package/dist/host/serve-daemon.js +501 -0
- package/dist/host/serve-entry.js +279 -0
- package/dist/host/session-anchor.js +41 -0
- package/dist/host/sessions-cmd.js +219 -0
- package/dist/host/sessions-face.js +47 -0
- package/dist/host/settings-store.js +112 -0
- package/dist/host/signals.js +69 -0
- package/dist/host/single-instance.js +105 -0
- package/dist/host/subagent-factory.js +291 -0
- package/dist/host/testkit/harness.js +283 -0
- package/dist/host/testkit/index.js +14 -0
- package/dist/host/testkit/matrix.js +302 -0
- package/dist/host/tool-policy-store.js +177 -0
- package/dist/host/triggers.js +212 -0
- package/dist/host/tui-entry.js +216 -0
- package/dist/host/webui-bridge.js +165 -0
- package/dist/issue/codes.js +38 -0
- package/dist/issue/filter.js +146 -0
- package/dist/issue/github.js +116 -0
- package/dist/issue/index.js +17 -0
- package/dist/issue/mount.js +70 -0
- package/dist/issue/poll.js +58 -0
- package/dist/issue/service.js +341 -0
- package/dist/issue/tools.js +70 -0
- package/dist/issue/types.js +34 -0
- package/dist/issue/webhook.js +126 -0
- package/dist/llm/codes.js +77 -0
- package/dist/llm/complete.js +196 -0
- package/dist/llm/events.js +15 -0
- package/dist/llm/index.js +24 -0
- package/dist/llm/inflight.js +65 -0
- package/dist/llm/model-id.js +58 -0
- package/dist/llm/provider-face.d.ts +32 -0
- package/dist/llm/provider-face.js +34 -0
- package/dist/llm/recovery.js +146 -0
- package/dist/llm/runtime.js +46 -0
- package/dist/llm/stream-fn.js +147 -0
- package/dist/lsp/codes.js +31 -0
- package/dist/lsp/connection.js +147 -0
- package/dist/lsp/frame.js +110 -0
- package/dist/lsp/index.js +16 -0
- package/dist/lsp/inject.js +58 -0
- package/dist/lsp/instance.js +264 -0
- package/dist/lsp/service.js +522 -0
- package/dist/lsp/tools.js +53 -0
- package/dist/lsp/types.js +201 -0
- package/dist/mcp/bridge.js +222 -0
- package/dist/mcp/codes.js +21 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/jsonrpc.js +230 -0
- package/dist/mcp/service.js +125 -0
- package/dist/mcp/tools.js +145 -0
- package/dist/mcp/types.js +108 -0
- package/dist/memory/cite.js +131 -0
- package/dist/memory/codes.js +50 -0
- package/dist/memory/command.js +71 -0
- package/dist/memory/consolidate.js +219 -0
- package/dist/memory/cycle.js +109 -0
- package/dist/memory/dao.js +878 -0
- package/dist/memory/diff.js +238 -0
- package/dist/memory/extract.js +181 -0
- package/dist/memory/fts.js +58 -0
- package/dist/memory/index.js +42 -0
- package/dist/memory/inject.js +354 -0
- package/dist/memory/merge.js +178 -0
- package/dist/memory/migration.js +132 -0
- package/dist/memory/pollution.js +59 -0
- package/dist/memory/port.js +385 -0
- package/dist/memory/review.js +201 -0
- package/dist/memory/scan.js +89 -0
- package/dist/memory/tools.js +508 -0
- package/dist/memory/types.js +140 -0
- package/dist/obs/codes.js +38 -0
- package/dist/obs/db.js +115 -0
- package/dist/obs/index.js +20 -0
- package/dist/obs/rollup.js +61 -0
- package/dist/obs/service.js +363 -0
- package/dist/obs/session-tools.js +282 -0
- package/dist/obs/session-view.js +225 -0
- package/dist/obs/tool.js +97 -0
- package/dist/obs/types.js +1 -0
- package/dist/persist/audit.js +63 -0
- package/dist/persist/aux.js +39 -0
- package/dist/persist/codes.js +35 -0
- package/dist/persist/index.js +30 -0
- package/dist/persist/load-history.js +50 -0
- package/dist/persist/migrations.js +37 -0
- package/dist/persist/paths.js +108 -0
- package/dist/persist/persistence.js +229 -0
- package/dist/persist/schema.js +99 -0
- package/dist/persist/secret-box.js +95 -0
- package/dist/persist/store.js +706 -0
- package/dist/persist/write-behind.js +238 -0
- package/dist/safety/approval.js +171 -0
- package/dist/safety/bwrap.js +123 -0
- package/dist/safety/codes.js +57 -0
- package/dist/safety/danger.js +505 -0
- package/dist/safety/gate.js +226 -0
- package/dist/safety/index.js +24 -0
- package/dist/safety/presets.js +73 -0
- package/dist/safety/roots.js +139 -0
- package/dist/safety/sandbox.js +214 -0
- package/dist/safety/seatbelt.js +88 -0
- package/dist/safety/sensitive.js +34 -0
- package/dist/safety/tool-policy.js +203 -0
- package/dist/safety/types.js +13 -0
- package/dist/scheduler/codes.js +54 -0
- package/dist/scheduler/cron-backend.js +135 -0
- package/dist/scheduler/engine.js +280 -0
- package/dist/scheduler/gates.js +60 -0
- package/dist/scheduler/index.js +25 -0
- package/dist/scheduler/migration.js +22 -0
- package/dist/scheduler/runner.js +171 -0
- package/dist/scheduler/schedule.js +193 -0
- package/dist/scheduler/service.js +283 -0
- package/dist/scheduler/tick.js +139 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/sdk/http.js +865 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/mcp.js +289 -0
- package/dist/sdk/plugin-route-registry.js +126 -0
- package/dist/sdk/plugin-routes.js +149 -0
- package/dist/sdk/security.js +116 -0
- package/dist/sdk/types.js +32 -0
- package/dist/session/budget.js +159 -0
- package/dist/session/codes.js +40 -0
- package/dist/session/derive.js +180 -0
- package/dist/session/event-data.js +1 -0
- package/dist/session/fork.js +72 -0
- package/dist/session/import-gates.js +148 -0
- package/dist/session/index.js +18 -0
- package/dist/session/recover.js +103 -0
- package/dist/session/session.js +279 -0
- package/dist/session/snapshot.js +107 -0
- package/dist/skills/agents.js +242 -0
- package/dist/skills/codes.js +51 -0
- package/dist/skills/discovery.js +266 -0
- package/dist/skills/frontmatter.js +215 -0
- package/dist/skills/index.js +28 -0
- package/dist/skills/load.js +118 -0
- package/dist/skills/manage.js +201 -0
- package/dist/skills/registry.js +138 -0
- package/dist/skills/render.js +124 -0
- package/dist/skills/sections.js +171 -0
- package/dist/skills/types.js +21 -0
- package/dist/subagent/codes.js +50 -0
- package/dist/subagent/declarative.js +55 -0
- package/dist/subagent/index.js +19 -0
- package/dist/subagent/notify.js +51 -0
- package/dist/subagent/provide.js +7 -0
- package/dist/subagent/registry.js +149 -0
- package/dist/subagent/service.js +338 -0
- package/dist/subagent/surface.js +44 -0
- package/dist/subagent/tool.js +153 -0
- package/dist/subagent/types.js +25 -0
- package/dist/tools/apply-patch.js +164 -0
- package/dist/tools/codes.js +109 -0
- package/dist/tools/fs.js +501 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/observed.js +99 -0
- package/dist/tools/pipeline.js +176 -0
- package/dist/tools/protected-read.js +54 -0
- package/dist/tools/registry.js +250 -0
- package/dist/tools/search.js +451 -0
- package/dist/tools/worktree.js +327 -0
- package/dist/web/codes.js +37 -0
- package/dist/web/gate.js +43 -0
- package/dist/web/hygiene.js +195 -0
- package/dist/web/index.js +16 -0
- package/dist/web/service.js +207 -0
- package/dist/web/ssrf-guard.js +44 -0
- package/dist/web/tool.js +76 -0
- package/dist/web/types.js +10 -0
- package/dist/webui/assets/index-B-t2O8gh.js +22 -0
- package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
- package/dist/webui/index.html +14 -0
- package/dist/webui/index.js +14 -0
- package/dist/webui/server.js +549 -0
- package/dist/webui/types.js +38 -0
- package/examples/README.md +26 -0
- package/examples/minimal-code-plugin/entry.js +43 -0
- package/examples/minimal-code-plugin/package.json +12 -0
- package/examples/pure-skill-pack/package.json +14 -0
- package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
- package/package.json +86 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* polluted 会话资格件(06 §4.1 + 批 18c-5 落码定形注——外部源内容不做记忆
|
|
3
|
+
* 提取源的资格检查;两路入口〔即时路编排首步/周期路 fire 首步〕同一检查)。
|
|
4
|
+
*
|
|
5
|
+
* **判据通配条目机制**:通配形 '*__*'('*' → 任意子串,无 '*' = 精确名);
|
|
6
|
+
* 起草值 `['fetch', 'mcp', '*__*']`——'fetch'/'mcp' 精确名(web fetch 工具与
|
|
7
|
+
* MCP 目录工具)+ '*__*' = MCP 复合键全族(`server__tool`——服务器键无
|
|
8
|
+
* 下划线,'__' 子串即复合键指纹)。清单常量可注入(判据演进不改机制)。
|
|
9
|
+
*
|
|
10
|
+
* **标记位 = tool/call 事件**(载荷携 name——周期路编排件消费 durable 事件
|
|
11
|
+
* 流时逐条喂 markIfPolluted;即时路不标记只查态)。**会话资格态 v1 = 进程
|
|
12
|
+
* 内存 Map**(重启回退 eligible——pollution 判定是会话进行时的护栏不是终审,
|
|
13
|
+
* 回退代价 = 至多一轮提取,不违背「durable 之外零新真值」红线:判据事件
|
|
14
|
+
* durable 而资格态是派生缓存)。**单向转移幂等**:eligible → polluted 一路,
|
|
15
|
+
* 无回退(污染不可逆——会话内外部源内容已混入,宁弃一轮不可误提)。
|
|
16
|
+
*/
|
|
17
|
+
import { MEMORY_POLLUTION_DEFAULT_PATTERNS } from './types.js';
|
|
18
|
+
/** 正则元字符转义(通配拆段后逐段转义——'*' 之外的元字符按字面量) */
|
|
19
|
+
function escapeRe(segment) {
|
|
20
|
+
return segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 通配判据匹配('*' → 任意子串):无 '*' = 精确名比对;含 '*' 按 `^…$` 全串
|
|
24
|
+
* 锚定('*__*' = 含 '__' 子串的任意名——MCP 复合键全族)。
|
|
25
|
+
*/
|
|
26
|
+
export function matchesToolPattern(toolName, pattern) {
|
|
27
|
+
if (!pattern.includes('*'))
|
|
28
|
+
return toolName === pattern;
|
|
29
|
+
const re = new RegExp(`^${pattern.split('*').map(escapeRe).join('.*')}$`);
|
|
30
|
+
return re.test(toolName);
|
|
31
|
+
}
|
|
32
|
+
/** 工具名是否命中 polluted 判据(缺省表 = 起草值;patterns 注入可覆盖) */
|
|
33
|
+
export function isPollutingToolName(toolName, patterns = MEMORY_POLLUTION_DEFAULT_PATTERNS) {
|
|
34
|
+
return patterns.some((p) => matchesToolPattern(toolName, p));
|
|
35
|
+
}
|
|
36
|
+
/** 建会话资格追踪器(进程单例——周期路编排件内建,装配面亦可外置注入) */
|
|
37
|
+
export function createPollutionTracker(deps = {}) {
|
|
38
|
+
const patterns = deps.patterns ?? MEMORY_POLLUTION_DEFAULT_PATTERNS;
|
|
39
|
+
const states = new Map();
|
|
40
|
+
return {
|
|
41
|
+
classify(sessionId) {
|
|
42
|
+
return states.get(sessionId) ?? 'eligible';
|
|
43
|
+
},
|
|
44
|
+
isPolluted(sessionId) {
|
|
45
|
+
return (states.get(sessionId) ?? 'eligible') === 'polluted';
|
|
46
|
+
},
|
|
47
|
+
markIfPolluted(sessionId, toolName) {
|
|
48
|
+
if ((states.get(sessionId) ?? 'eligible') === 'polluted')
|
|
49
|
+
return false; // 幂等——已污染零动作
|
|
50
|
+
if (!isPollutingToolName(toolName, patterns))
|
|
51
|
+
return false;
|
|
52
|
+
states.set(sessionId, 'polluted'); // 单向转移——无回退
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
pollutedSessions() {
|
|
56
|
+
return [...states.entries()].filter(([, s]) => s === 'polluted').map(([id]) => id);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory 导入导出件(批 18c-8——06 §3 文件导入导出条 + 落码定形注)。
|
|
3
|
+
*
|
|
4
|
+
* **格式**:JSONL + 首行 header meta(magic 串 `berry-agent-memory`、
|
|
5
|
+
* formatVersion 2——批 ev-1 版本链随包升版;v1 旧件按版本判读收〔无
|
|
6
|
+
* versions/valid_from 键〕——判读靠 formatVersion 字段正是「第一天定死
|
|
7
|
+
* 不留旧格式匹配兼容层」的机制用途);数据行 = 蛇列名全列(与 DDL 同源的
|
|
8
|
+
* 持久互操作词面),全状态现行值按 id 升序确定性排列(可 diff)。
|
|
9
|
+
*
|
|
10
|
+
* **导入 = 恢复式语义**:按 id 幂等(无此 id 直插;有则跳过零合并零覆写);
|
|
11
|
+
* 行级尽力而为四账(inserted/skippedExisting/rejectedSecret/
|
|
12
|
+
* rejectedMalformed——坏形行不弃批,整文件拒只留给 header 坏形
|
|
13
|
+
* MEMORY_IMPORT_FORMAT_INVALID)。secret 扫描与三写事务在 DAO
|
|
14
|
+
* importInsert 单点执法(导入面即写入面)。
|
|
15
|
+
*
|
|
16
|
+
* 可写根判定 isWithinRoots **同律复用引证**:函数体拷贝自 skills/manage.ts
|
|
17
|
+
* 先例(memory 与 skills 无 DAG 边不引 import——漂移由对拍测试互证;
|
|
18
|
+
* SKILLS_WRITE_ROOT_DENIED / FS_OUTSIDE_WRITABLE_ROOTS 同律先例)。
|
|
19
|
+
*/
|
|
20
|
+
import { resolve, sep } from 'node:path';
|
|
21
|
+
import { BaseError } from '../contracts/index.js';
|
|
22
|
+
import { MEMORY_EXPORT_FORMAT_VERSION, MEMORY_EXPORT_MAGIC, MEMORY_KINDS, MEMORY_STATUSES, } from './types.js';
|
|
23
|
+
/**
|
|
24
|
+
* 可写根判定(导出落盘路径越界拒——resolve 归一 + 边界分隔符守卫)。
|
|
25
|
+
* 同律复用引证:拷贝自 skills/manage.ts isWithinRoots,语义钉死同款——
|
|
26
|
+
* 根本体等于路径或以 `<root>/` 开头才算界内(`/root-x` 不在 `/root` 内)。
|
|
27
|
+
*/
|
|
28
|
+
export function isWithinRoots(path, roots) {
|
|
29
|
+
const abs = resolve(path);
|
|
30
|
+
return roots.some((root) => {
|
|
31
|
+
const base = resolve(root);
|
|
32
|
+
return abs === base || abs.startsWith(base + sep);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** 驼峰行 → 蛇列 JSONL 行(词面与 DDL 同源;值形取解析形——source_refs 数组、frozen 布尔;versions 批 ev-1 随包) */
|
|
36
|
+
export function exportRowOf(row, versions) {
|
|
37
|
+
return {
|
|
38
|
+
id: row.id,
|
|
39
|
+
owner_key: row.ownerKey,
|
|
40
|
+
kind: row.kind,
|
|
41
|
+
summary: row.summary,
|
|
42
|
+
content: row.content,
|
|
43
|
+
confidence: row.confidence,
|
|
44
|
+
evidence_count: row.evidenceCount,
|
|
45
|
+
status: row.status,
|
|
46
|
+
superseded_by: row.supersededBy,
|
|
47
|
+
source_refs: row.sourceRefs,
|
|
48
|
+
created_at: row.createdAt,
|
|
49
|
+
updated_at: row.updatedAt,
|
|
50
|
+
usage_count: row.usageCount,
|
|
51
|
+
last_used_at: row.lastUsedAt,
|
|
52
|
+
corrected_count: row.correctedCount,
|
|
53
|
+
frozen: row.frozen,
|
|
54
|
+
ttl_days: row.ttlDays,
|
|
55
|
+
expires_at: row.expiresAt,
|
|
56
|
+
valid_from: row.validFrom,
|
|
57
|
+
...(versions !== undefined
|
|
58
|
+
? {
|
|
59
|
+
versions: versions.map((v) => ({
|
|
60
|
+
id: v.id,
|
|
61
|
+
revision: v.revision,
|
|
62
|
+
cause: v.cause,
|
|
63
|
+
reason: v.reason,
|
|
64
|
+
owner_key: v.ownerKey,
|
|
65
|
+
kind: v.kind,
|
|
66
|
+
summary: v.summary,
|
|
67
|
+
content: v.content,
|
|
68
|
+
confidence: v.confidence,
|
|
69
|
+
evidence_count: v.evidenceCount,
|
|
70
|
+
created_at: v.createdAt,
|
|
71
|
+
})),
|
|
72
|
+
}
|
|
73
|
+
: {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 导出序列化纯函数(header + 行按入参序确定性输出——同库两次导出文本
|
|
78
|
+
* 恒等除 exportedAt;行已由 DAO 按 id 升序取数)。尾行换行收口(POSIX 文本件惯例)。
|
|
79
|
+
* versionsOf 注入位(批 ev-1——装配面恒接 dao.versions;缺省行不带 versions 键)。
|
|
80
|
+
*/
|
|
81
|
+
export function serializeMemoryExport(meta, rows, versionsOf) {
|
|
82
|
+
const header = {
|
|
83
|
+
format: MEMORY_EXPORT_MAGIC,
|
|
84
|
+
formatVersion: MEMORY_EXPORT_FORMAT_VERSION,
|
|
85
|
+
...meta,
|
|
86
|
+
};
|
|
87
|
+
return ([JSON.stringify(header), ...rows.map((row) => JSON.stringify(exportRowOf(row, versionsOf?.(row.id))))].join('\n') +
|
|
88
|
+
'\n');
|
|
89
|
+
}
|
|
90
|
+
/** header 坏形整文件拒(唯一 MEMORY_IMPORT_FORMAT_INVALID 抛点——判据:magic 不符或 formatVersion ∉ {1,2} 或 meta 字段坏形) */
|
|
91
|
+
function invalidHeader(detail) {
|
|
92
|
+
return new BaseError('MEMORY_IMPORT_FORMAT_INVALID', `导入文件 header 坏形整文件拒(${detail})`);
|
|
93
|
+
}
|
|
94
|
+
/** formatVersion 可收值(批 ev-1——v1 旧件判读收;双收判定单源) */
|
|
95
|
+
const FORMAT_VERSIONS = [1, 2];
|
|
96
|
+
/** 首行 header 解析(magic/formatVersion/exportedAt/ownerScope/ownerRoots 五字段全检) */
|
|
97
|
+
export function parseMemoryImportHeader(line) {
|
|
98
|
+
let parsed;
|
|
99
|
+
try {
|
|
100
|
+
parsed = JSON.parse(line);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
throw invalidHeader('首行非 JSON');
|
|
104
|
+
}
|
|
105
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
106
|
+
throw invalidHeader('首行非对象');
|
|
107
|
+
}
|
|
108
|
+
const o = parsed;
|
|
109
|
+
if (o['format'] !== MEMORY_EXPORT_MAGIC)
|
|
110
|
+
throw invalidHeader(`magic 串不符:${String(o['format'])}`);
|
|
111
|
+
if (!FORMAT_VERSIONS.includes(o['formatVersion'])) {
|
|
112
|
+
throw invalidHeader(`formatVersion ∉ {1,2}:${String(o['formatVersion'])}`);
|
|
113
|
+
}
|
|
114
|
+
if (!Number.isInteger(o['exportedAt']) || o['exportedAt'] < 0) {
|
|
115
|
+
throw invalidHeader(`exportedAt 坏形(非负整数):${String(o['exportedAt'])}`);
|
|
116
|
+
}
|
|
117
|
+
if (typeof o['ownerScope'] !== 'string' || o['ownerScope'] === '') {
|
|
118
|
+
throw invalidHeader(`ownerScope 坏形:${String(o['ownerScope'])}`);
|
|
119
|
+
}
|
|
120
|
+
const roots = o['ownerRoots'];
|
|
121
|
+
if (roots === null || typeof roots !== 'object' || Array.isArray(roots)) {
|
|
122
|
+
throw invalidHeader('ownerRoots 坏形(对象形)');
|
|
123
|
+
}
|
|
124
|
+
for (const [key, value] of Object.entries(roots)) {
|
|
125
|
+
if (typeof value !== 'string')
|
|
126
|
+
throw invalidHeader(`ownerRoots[${key}] 坏形(字符串值):${String(value)}`);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
format: MEMORY_EXPORT_MAGIC,
|
|
130
|
+
formatVersion: o['formatVersion'],
|
|
131
|
+
exportedAt: o['exportedAt'],
|
|
132
|
+
ownerScope: o['ownerScope'],
|
|
133
|
+
ownerRoots: roots,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** 行坏形(MEMORY_ENTRY_INVALID——行级宽容分账 rejectedMalformed,不弃批) */
|
|
137
|
+
function invalidRow(lineNo, problems) {
|
|
138
|
+
return new BaseError('MEMORY_ENTRY_INVALID', `导入行坏形(第 ${lineNo} 行):${problems}`);
|
|
139
|
+
}
|
|
140
|
+
/** 非负整数判定(时间戳/计数字段共用) */
|
|
141
|
+
function isNonNegInt(v) {
|
|
142
|
+
return typeof v === 'number' && Number.isInteger(v) && v >= 0;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 数据行解析(词法判定全清单——18 列在场〔corrected_count 为**容错位**:旧 17 列
|
|
146
|
+
* 文件缺席按 DEFAULT 0 收、不折坏形;在场须非负整数〕+ 类型 + 闭集 + 区间)。
|
|
147
|
+
* 行级坏形抛 MEMORY_ENTRY_INVALID(与 ingest 坏形同码——runMemoryImport
|
|
148
|
+
* 折 rejectedMalformed 分账)。
|
|
149
|
+
*/
|
|
150
|
+
export function parseMemoryImportRow(line, lineNo) {
|
|
151
|
+
let parsed;
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(line);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
throw invalidRow(lineNo, '非 JSON');
|
|
157
|
+
}
|
|
158
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
159
|
+
throw invalidRow(lineNo, '非对象');
|
|
160
|
+
}
|
|
161
|
+
const o = parsed;
|
|
162
|
+
const problems = [];
|
|
163
|
+
const str = (key) => {
|
|
164
|
+
const v = o[key];
|
|
165
|
+
if (typeof v !== 'string' || v === '') {
|
|
166
|
+
problems.push(`${key} 坏形(非空字符串)`);
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
return v;
|
|
170
|
+
};
|
|
171
|
+
const id = str('id');
|
|
172
|
+
const ownerKey = str('owner_key');
|
|
173
|
+
const summary = str('summary');
|
|
174
|
+
const content = str('content');
|
|
175
|
+
const kind = o['kind'];
|
|
176
|
+
if (typeof kind !== 'string' || !MEMORY_KINDS.includes(kind)) {
|
|
177
|
+
problems.push(`kind 非七值闭集:${String(kind)}`);
|
|
178
|
+
}
|
|
179
|
+
const status = o['status'];
|
|
180
|
+
if (typeof status !== 'string' || !MEMORY_STATUSES.includes(status)) {
|
|
181
|
+
problems.push(`status 非三值闭集:${String(status)}`);
|
|
182
|
+
}
|
|
183
|
+
const confidence = o['confidence'];
|
|
184
|
+
if (typeof confidence !== 'number' || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
|
|
185
|
+
problems.push(`confidence 越界 [0,1]:${String(confidence)}`);
|
|
186
|
+
}
|
|
187
|
+
const evidence = o['evidence_count'];
|
|
188
|
+
if (!isNonNegInt(evidence) || evidence < 1)
|
|
189
|
+
problems.push(`evidence_count 坏形(≥1 整数):${String(evidence)}`);
|
|
190
|
+
const supersededBy = o['superseded_by'];
|
|
191
|
+
if (supersededBy !== null && typeof supersededBy !== 'string') {
|
|
192
|
+
problems.push('superseded_by 坏形(字符串或 null)');
|
|
193
|
+
}
|
|
194
|
+
const refs = o['source_refs'];
|
|
195
|
+
let sourceRefs = [];
|
|
196
|
+
if (!Array.isArray(refs)) {
|
|
197
|
+
problems.push('source_refs 坏形(数组)');
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
for (const [i, ref] of refs.entries()) {
|
|
201
|
+
if (ref === null ||
|
|
202
|
+
typeof ref !== 'object' ||
|
|
203
|
+
typeof ref['sessionId'] !== 'string' ||
|
|
204
|
+
ref['sessionId'] === '' ||
|
|
205
|
+
!isNonNegInt(ref['seq'])) {
|
|
206
|
+
problems.push(`source_refs[${i}] 坏形({sessionId, seq} 形外)`);
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
sourceRefs = refs;
|
|
211
|
+
}
|
|
212
|
+
if (!isNonNegInt(o['created_at']))
|
|
213
|
+
problems.push(`created_at 坏形(非负整数):${String(o['created_at'])}`);
|
|
214
|
+
if (!isNonNegInt(o['updated_at']))
|
|
215
|
+
problems.push(`updated_at 坏形(非负整数):${String(o['updated_at'])}`);
|
|
216
|
+
const usage = o['usage_count'];
|
|
217
|
+
if (!isNonNegInt(usage))
|
|
218
|
+
problems.push(`usage_count 坏形(非负整数):${String(usage)}`);
|
|
219
|
+
const lastUsed = o['last_used_at'];
|
|
220
|
+
if (lastUsed !== null && !isNonNegInt(lastUsed))
|
|
221
|
+
problems.push(`last_used_at 坏形(非负整数或 null):${String(lastUsed)}`);
|
|
222
|
+
// corrected_count 容错位(2026-09-08 消化批——冷读闸 major 销账):缺席 = 旧
|
|
223
|
+
// 17 列文件按 DEFAULT 0 收(缺席不折坏形);在场须非负整数
|
|
224
|
+
const corrected = o['corrected_count'];
|
|
225
|
+
if (corrected !== undefined && corrected !== null && !isNonNegInt(corrected)) {
|
|
226
|
+
problems.push(`corrected_count 坏形(非负整数):${String(corrected)}`);
|
|
227
|
+
}
|
|
228
|
+
const frozen = o['frozen'];
|
|
229
|
+
if (typeof frozen !== 'boolean')
|
|
230
|
+
problems.push(`frozen 坏形(布尔):${String(frozen)}`);
|
|
231
|
+
const ttlDays = o['ttl_days'];
|
|
232
|
+
if (ttlDays !== null && (!Number.isInteger(ttlDays) || ttlDays < 1)) {
|
|
233
|
+
problems.push(`ttl_days 坏形(正整数或 null):${String(ttlDays)}`);
|
|
234
|
+
}
|
|
235
|
+
const expiresAt = o['expires_at'];
|
|
236
|
+
if (expiresAt !== null && !isNonNegInt(expiresAt)) {
|
|
237
|
+
problems.push(`expires_at 坏形(非负整数或 null):${String(expiresAt)}`);
|
|
238
|
+
}
|
|
239
|
+
// valid_from 容错位(批 ev-1——v1/早期 v2 旧件缺席按 NULL 收、不折坏形;在场须非负整数或 null)
|
|
240
|
+
const validFrom = o['valid_from'];
|
|
241
|
+
if (validFrom !== undefined && validFrom !== null && !isNonNegInt(validFrom)) {
|
|
242
|
+
problems.push(`valid_from 坏形(非负整数或 null):${String(validFrom)}`);
|
|
243
|
+
}
|
|
244
|
+
// versions 随包链(批 ev-1——v1 旧件无此键缺席收;在场须数组且逐行词法全检)
|
|
245
|
+
const rawVersions = o['versions'];
|
|
246
|
+
let versions;
|
|
247
|
+
if (rawVersions !== undefined) {
|
|
248
|
+
if (!Array.isArray(rawVersions)) {
|
|
249
|
+
problems.push('versions 坏形(数组)');
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const parsedVersions = [];
|
|
253
|
+
const seenRevisions = new Set();
|
|
254
|
+
for (const [i, raw] of rawVersions.entries()) {
|
|
255
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
256
|
+
problems.push(`versions[${i}] 坏形(对象形)`);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
const v = raw;
|
|
260
|
+
const vProblems = [];
|
|
261
|
+
if (typeof v['id'] !== 'string' || v['id'] === '')
|
|
262
|
+
vProblems.push('id 坏形(非空字符串)');
|
|
263
|
+
if (!Number.isInteger(v['revision']) || v['revision'] < 1) {
|
|
264
|
+
vProblems.push(`revision 坏形(正整数):${String(v['revision'])}`);
|
|
265
|
+
}
|
|
266
|
+
else if (seenRevisions.has(v['revision'])) {
|
|
267
|
+
vProblems.push(`revision 重复:${String(v['revision'])}`);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
seenRevisions.add(v['revision']);
|
|
271
|
+
}
|
|
272
|
+
if (v['cause'] !== 'insert' && v['cause'] !== 'merge' && v['cause'] !== 'decay' && v['cause'] !== 'rollback') {
|
|
273
|
+
vProblems.push(`cause 非四值闭集:${String(v['cause'])}`);
|
|
274
|
+
}
|
|
275
|
+
if (v['reason'] !== null && v['reason'] !== undefined && typeof v['reason'] !== 'string') {
|
|
276
|
+
vProblems.push(`reason 坏形(字符串或 null):${String(v['reason'])}`);
|
|
277
|
+
}
|
|
278
|
+
if (typeof v['owner_key'] !== 'string' || v['owner_key'] === '')
|
|
279
|
+
vProblems.push('owner_key 坏形(非空字符串)');
|
|
280
|
+
if (typeof v['kind'] !== 'string' || !MEMORY_KINDS.includes(v['kind'])) {
|
|
281
|
+
vProblems.push(`kind 非七值闭集:${String(v['kind'])}`);
|
|
282
|
+
}
|
|
283
|
+
if (typeof v['summary'] !== 'string' || v['summary'] === '')
|
|
284
|
+
vProblems.push('summary 坏形(非空字符串)');
|
|
285
|
+
if (typeof v['content'] !== 'string' || v['content'] === '')
|
|
286
|
+
vProblems.push('content 坏形(非空字符串)');
|
|
287
|
+
if (typeof v['confidence'] !== 'number' ||
|
|
288
|
+
!Number.isFinite(v['confidence']) ||
|
|
289
|
+
v['confidence'] < 0 ||
|
|
290
|
+
v['confidence'] > 1) {
|
|
291
|
+
vProblems.push(`confidence 越界 [0,1]:${String(v['confidence'])}`);
|
|
292
|
+
}
|
|
293
|
+
if (!isNonNegInt(v['evidence_count']) || v['evidence_count'] < 1) {
|
|
294
|
+
vProblems.push(`evidence_count 坏形(≥1 整数):${String(v['evidence_count'])}`);
|
|
295
|
+
}
|
|
296
|
+
if (!isNonNegInt(v['created_at']))
|
|
297
|
+
vProblems.push(`created_at 坏形(非负整数):${String(v['created_at'])}`);
|
|
298
|
+
if (vProblems.length > 0) {
|
|
299
|
+
problems.push(`versions[${i}]:${vProblems.join(';')}`);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
parsedVersions.push({
|
|
303
|
+
id: v['id'],
|
|
304
|
+
revision: v['revision'],
|
|
305
|
+
cause: v['cause'],
|
|
306
|
+
reason: v['reason'] ?? null,
|
|
307
|
+
owner_key: v['owner_key'],
|
|
308
|
+
kind: v['kind'],
|
|
309
|
+
summary: v['summary'],
|
|
310
|
+
content: v['content'],
|
|
311
|
+
confidence: v['confidence'],
|
|
312
|
+
evidence_count: v['evidence_count'],
|
|
313
|
+
created_at: v['created_at'],
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (parsedVersions.length === rawVersions.length)
|
|
317
|
+
versions = parsedVersions;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (problems.length > 0)
|
|
321
|
+
throw invalidRow(lineNo, problems.join(';'));
|
|
322
|
+
return {
|
|
323
|
+
id: id,
|
|
324
|
+
owner_key: ownerKey,
|
|
325
|
+
kind: kind,
|
|
326
|
+
summary: summary,
|
|
327
|
+
content: content,
|
|
328
|
+
confidence: confidence,
|
|
329
|
+
evidence_count: evidence,
|
|
330
|
+
status: status,
|
|
331
|
+
superseded_by: supersededBy ?? null,
|
|
332
|
+
source_refs: sourceRefs,
|
|
333
|
+
created_at: o['created_at'],
|
|
334
|
+
updated_at: o['updated_at'],
|
|
335
|
+
usage_count: usage,
|
|
336
|
+
last_used_at: lastUsed ?? null,
|
|
337
|
+
...(isNonNegInt(corrected) ? { corrected_count: corrected } : {}),
|
|
338
|
+
frozen: frozen,
|
|
339
|
+
ttl_days: ttlDays ?? null,
|
|
340
|
+
expires_at: expiresAt ?? null,
|
|
341
|
+
...(validFrom !== undefined ? { valid_from: validFrom ?? null } : {}),
|
|
342
|
+
...(versions !== undefined ? { versions } : {}),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* 恢复式导入编排(行级尽力而为四账):首行 header 坏形整文件拒
|
|
347
|
+
* (MEMORY_IMPORT_FORMAT_INVALID 唯一外抛位);数据行逐条「解析 →
|
|
348
|
+
* DAO importInsert(secret 扫描 + 三写事务单点执法)」——secret 命中折
|
|
349
|
+
* rejectedSecret、行坏形折 rejectedMalformed、id 已在折 skippedExisting、
|
|
350
|
+
* 落库成功计 inserted。空行宽容跳过(尾行换行/编辑器空行不折坏形)。
|
|
351
|
+
*/
|
|
352
|
+
export function runMemoryImport(text, dao) {
|
|
353
|
+
const lines = text.split('\n').filter((line) => line.trim() !== '');
|
|
354
|
+
if (lines.length === 0) {
|
|
355
|
+
throw invalidHeader('文件空(首行 header 缺席)');
|
|
356
|
+
}
|
|
357
|
+
parseMemoryImportHeader(lines[0]);
|
|
358
|
+
const outcome = { inserted: 0, skippedExisting: 0, rejectedSecret: 0, rejectedMalformed: 0 };
|
|
359
|
+
for (const [index, line] of lines.slice(1).entries()) {
|
|
360
|
+
const lineNo = index + 2; // 文件行号(1 起)——header 占首行
|
|
361
|
+
try {
|
|
362
|
+
const row = parseMemoryImportRow(line, lineNo);
|
|
363
|
+
if (dao.importInsert(row))
|
|
364
|
+
outcome.inserted += 1;
|
|
365
|
+
else
|
|
366
|
+
outcome.skippedExisting += 1;
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
if (err instanceof BaseError && err.code === 'MEMORY_SECRET_DETECTED')
|
|
370
|
+
outcome.rejectedSecret += 1;
|
|
371
|
+
else if (err instanceof BaseError && err.code === 'MEMORY_ENTRY_INVALID')
|
|
372
|
+
outcome.rejectedMalformed += 1;
|
|
373
|
+
else
|
|
374
|
+
throw err; // 非词法面异常照常外炸(DB/IO 错误不吞)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return outcome;
|
|
378
|
+
}
|
|
379
|
+
/** 导出编排(取数 → 序列化——ownerKey 缺省 = 全 owner;ownerRoots 由命令 deps 装配闭包注入) */
|
|
380
|
+
export function buildMemoryExport(dao, opts) {
|
|
381
|
+
const rows = dao.listForExport(opts.ownerKey);
|
|
382
|
+
return serializeMemoryExport({ exportedAt: opts.now, ownerScope: opts.ownerKey ?? 'all', ownerRoots: opts.ownerRoots }, rows,
|
|
383
|
+
// 批 ev-1——版本链随包导出(跨机往返后 restore revision 回滚面的前提)
|
|
384
|
+
(id) => dao.versions(id));
|
|
385
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { isEligibleUserSource, userTextOf } from './extract.js';
|
|
2
|
+
import { MEMORY_CONTENT_MAX_CHARS, MEMORY_REVIEW_CONFIDENCE, MEMORY_SUMMARY_MAX_CHARS, REVIEW_KINDS, llmTextOf, } from './types.js';
|
|
3
|
+
import { Type } from 'typebox';
|
|
4
|
+
import { Value } from 'typebox/value';
|
|
5
|
+
/* ---------------- JSON 三试(共享纯函数——consolidate 复用) ---------------- */
|
|
6
|
+
/** 代码围栏剥取(```json … ``` / ``` … ```——内层再入三试链) */
|
|
7
|
+
function stripFence(text) {
|
|
8
|
+
const m = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
9
|
+
return m !== null ? m[1] : null;
|
|
10
|
+
}
|
|
11
|
+
/** 首平衡段扫描(首 '{'/'[' 起配平到对应闭括——字符串字面量与转义跳越) */
|
|
12
|
+
function balancedSegment(text) {
|
|
13
|
+
const open = text.search(/[{[]/);
|
|
14
|
+
if (open < 0)
|
|
15
|
+
return null;
|
|
16
|
+
const openCh = text[open];
|
|
17
|
+
const closeCh = openCh === '{' ? '}' : ']';
|
|
18
|
+
let depth = 0;
|
|
19
|
+
let inString = false;
|
|
20
|
+
let escaped = false;
|
|
21
|
+
for (let i = open; i < text.length; i++) {
|
|
22
|
+
const ch = text[i];
|
|
23
|
+
if (inString) {
|
|
24
|
+
if (escaped)
|
|
25
|
+
escaped = false;
|
|
26
|
+
else if (ch === '\\')
|
|
27
|
+
escaped = true;
|
|
28
|
+
else if (ch === '"')
|
|
29
|
+
inString = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (ch === '"')
|
|
33
|
+
inString = true;
|
|
34
|
+
else if (ch === openCh)
|
|
35
|
+
depth++;
|
|
36
|
+
else if (ch === closeCh) {
|
|
37
|
+
depth--;
|
|
38
|
+
if (depth === 0)
|
|
39
|
+
return text.slice(open, i + 1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* JSON 载荷三试解析(定形注:直取/剥围栏/平衡段):任一试 JSON.parse 成功即
|
|
46
|
+
* 返回该值(不校验形状——形状校验归调用方 TypeBox 面);三试皆败返回 null。
|
|
47
|
+
*/
|
|
48
|
+
export function parseJsonPayload(text) {
|
|
49
|
+
// 试一:直取
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(text);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* 落试二 */
|
|
55
|
+
}
|
|
56
|
+
// 试二:剥代码围栏
|
|
57
|
+
const fenced = stripFence(text);
|
|
58
|
+
if (fenced !== null) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(fenced);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* 落试三 */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// 试三:首平衡段(模型前后缀闲话包裹的载荷)
|
|
67
|
+
const segment = balancedSegment(text);
|
|
68
|
+
if (segment !== null) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(segment);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* 三试皆败 */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** user/message 载荷窄读(结构兼容 durable 载荷 {content, source?}——词面独立律零 session import) */
|
|
79
|
+
function userMessageData(event) {
|
|
80
|
+
const data = event.data;
|
|
81
|
+
if (data === null || typeof data !== 'object' || data.content === undefined)
|
|
82
|
+
return null;
|
|
83
|
+
const content = data.content;
|
|
84
|
+
return { content, ...(typeof data.source === 'string' ? { source: data.source } : {}) };
|
|
85
|
+
}
|
|
86
|
+
/** assistant/message 载荷窄读(content 块数组——text 块拼接复用 userTextOf) */
|
|
87
|
+
function assistantText(event) {
|
|
88
|
+
const data = event.data;
|
|
89
|
+
if (data === null || typeof data !== 'object' || !Array.isArray(data.content))
|
|
90
|
+
return null;
|
|
91
|
+
return userTextOf(data.content);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 审阅窗转录(06 §4 周期路——只转录文本面两腿):user/message 过机器源滤除
|
|
95
|
+
* 与文本提取;assistant/message 拼 text 块;其余事件类型(tool 面/turn 面/
|
|
96
|
+
* 未知类型)一律不转录。anchorSeq = 首转录行 seq(候选缺省溯源位)。
|
|
97
|
+
*/
|
|
98
|
+
export function transcribeForReview(events) {
|
|
99
|
+
const items = [];
|
|
100
|
+
for (const event of events) {
|
|
101
|
+
if (event.type === 'user/message') {
|
|
102
|
+
const data = userMessageData(event);
|
|
103
|
+
if (data === null)
|
|
104
|
+
continue;
|
|
105
|
+
if (!isEligibleUserSource(data.source))
|
|
106
|
+
continue; // 机器载体不入审阅面
|
|
107
|
+
const text = userTextOf(data.content);
|
|
108
|
+
if (text === null)
|
|
109
|
+
continue;
|
|
110
|
+
items.push({ seq: event.seq, role: 'user', text });
|
|
111
|
+
}
|
|
112
|
+
else if (event.type === 'assistant/message') {
|
|
113
|
+
const text = assistantText(event);
|
|
114
|
+
if (text === null || text === '')
|
|
115
|
+
continue;
|
|
116
|
+
items.push({ seq: event.seq, role: 'assistant', text });
|
|
117
|
+
}
|
|
118
|
+
// 其余类型不转录(转录纪律——tool 面不进 LLM prompt)
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
items,
|
|
122
|
+
anchorSeq: items.length > 0 ? items[0].seq : null,
|
|
123
|
+
seqs: new Set(items.map((i) => i.seq)),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/* ---------------- 周期路编排(LLM 拍 + 按条校验入库) ---------------- */
|
|
127
|
+
/** review 候选条 schema(TypeBox 深校验——未知字段拒收;单条坏形按条丢弃) */
|
|
128
|
+
const REVIEW_ITEM = Type.Object({
|
|
129
|
+
kind: Type.Union(REVIEW_KINDS.map((k) => Type.Literal(k))),
|
|
130
|
+
summary: Type.String({ minLength: 1, maxLength: MEMORY_SUMMARY_MAX_CHARS }),
|
|
131
|
+
content: Type.String({ minLength: 1, maxLength: MEMORY_CONTENT_MAX_CHARS }),
|
|
132
|
+
confidence: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
|
|
133
|
+
sourceSeq: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
134
|
+
}, { additionalProperties: false });
|
|
135
|
+
/** review 系统提示词(只出 JSON 数组——五类闭集 + sourceSeq 指认) */
|
|
136
|
+
const REVIEW_SYSTEM_PROMPT = [
|
|
137
|
+
'你是记忆审阅器:从会话转录中提取值得长期保留的记忆候选。',
|
|
138
|
+
'只输出一个 JSON 数组(无其他文本),每项形如',
|
|
139
|
+
'{"kind":"preference|fact|convention|failure|insight","summary":"一句话摘要","content":"全文","confidence":0.0到1.0,"sourceSeq":对应转录行的 seq}',
|
|
140
|
+
'规则:只提取明确可复用的知识(用户偏好/事实/约定/失败教训/洞见);不提取一次性细节、猜测或任何敏感信息;',
|
|
141
|
+
'summary 与 content 使用转录原语言;同一知识只提取一条;无值得提取的内容输出 []。',
|
|
142
|
+
].join('\n');
|
|
143
|
+
/**
|
|
144
|
+
* 跑一轮周期路 review(06 §4——候选逐条经 ingest 合并管线入库,不直接 insert):
|
|
145
|
+
* 转录 → 预算闸 → LLM 拍 → JSON 三试 → TypeBox 按条校验(按条丢弃不弃批)→
|
|
146
|
+
* 逐条 ingest(owner 恒 global;置信度缺省 0.6;溯源 sourceSeq 越窗回落锚)。
|
|
147
|
+
*/
|
|
148
|
+
export async function runMemoryReview(deps, sessionId, events) {
|
|
149
|
+
const warn = deps.warn ?? (() => { });
|
|
150
|
+
const transcript = transcribeForReview(events);
|
|
151
|
+
if (transcript.items.length === 0)
|
|
152
|
+
return { outcome: 'skipped-empty', extracted: 0, discarded: 0 };
|
|
153
|
+
// 预算闸(06 §4 字面:预算不足跳过本轮——下个周期再试)
|
|
154
|
+
if (!deps.llm.canAfford('background'))
|
|
155
|
+
return { outcome: 'skipped-budget', extracted: 0, discarded: 0 };
|
|
156
|
+
const lines = transcript.items.map((i) => `[seq=${i.seq} ${i.role}] ${i.text}`).join('\n');
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
const completion = await deps.llm.complete({
|
|
160
|
+
systemPrompt: REVIEW_SYSTEM_PROMPT,
|
|
161
|
+
messages: [{ role: 'user', content: `会话 ${sessionId} 审阅窗转录:\n${lines}` }],
|
|
162
|
+
priority: 'background',
|
|
163
|
+
});
|
|
164
|
+
parsed = parseJsonPayload(llmTextOf(completion.message.content));
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
warn(`memory 周期路 review 拍失败(跳过本轮):${error instanceof Error ? error.message : String(error)}`);
|
|
168
|
+
return { outcome: 'skipped-error', extracted: 0, discarded: 0 };
|
|
169
|
+
}
|
|
170
|
+
if (!Array.isArray(parsed))
|
|
171
|
+
return { outcome: 'skipped-parse', extracted: 0, discarded: 0 };
|
|
172
|
+
let extracted = 0;
|
|
173
|
+
let discarded = 0;
|
|
174
|
+
const anchor = transcript.anchorSeq ?? 0; // 无转录行已早退——恒非 null 到此
|
|
175
|
+
for (const raw of parsed) {
|
|
176
|
+
// 按条校验(TypeBox 深校验未知字段拒收——坏形只丢该条不弃批)
|
|
177
|
+
if (typeof raw !== 'object' || raw === null || !Value.Check(REVIEW_ITEM, raw)) {
|
|
178
|
+
discarded++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const item = raw;
|
|
182
|
+
// 溯源:sourceSeq ∈ 窗内转录行取该行;缺席/越窗(幻觉)回落审阅窗锚
|
|
183
|
+
const seq = item.sourceSeq !== undefined && transcript.seqs.has(item.sourceSeq) ? item.sourceSeq : anchor;
|
|
184
|
+
try {
|
|
185
|
+
deps.dao.ingest({
|
|
186
|
+
ownerKey: 'global',
|
|
187
|
+
kind: item.kind,
|
|
188
|
+
summary: item.summary,
|
|
189
|
+
content: item.content,
|
|
190
|
+
confidence: item.confidence ?? MEMORY_REVIEW_CONFIDENCE,
|
|
191
|
+
sourceRefs: [{ sessionId, seq }],
|
|
192
|
+
});
|
|
193
|
+
extracted++;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// 入库拒(写前 secret 扫描命中/坏形——Value.Check 已挡形面,此处余 secret 面)
|
|
197
|
+
discarded++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { outcome: 'ran', extracted, discarded };
|
|
201
|
+
}
|