@bolloon/bolloon-agent 0.4.33 → 0.5.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/dist/agents/contacts/chain.js +27 -7
- package/dist/agents/execution-supervisor.js +580 -40
- package/dist/agents/external-events.js +35 -6
- package/dist/agents/gateway-group.js +100 -12
- package/dist/agents/goal-criteria.js +10 -3
- package/dist/agents/goal-flywheel/block-executor.js +346 -0
- package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
- package/dist/agents/goal-flywheel/goal-change.js +754 -0
- package/dist/agents/goal-flywheel/index.js +26 -0
- package/dist/agents/goal-flywheel/memory-layers.js +227 -0
- package/dist/agents/goal-flywheel/run-closure.js +809 -0
- package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
- package/dist/agents/goal-flywheel/types.js +329 -0
- package/dist/agents/goal-flywheel/wiring/change.js +288 -0
- package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
- package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
- package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
- package/dist/agents/goal-flywheel/wiring/index.js +26 -0
- package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
- package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
- package/dist/agents/goal-flywheel/work-contract.js +478 -0
- package/dist/agents/goal-flywheel/work-monitor.js +488 -0
- package/dist/agents/goal-flywheel-wiring.js +1870 -0
- package/dist/agents/goal-state-reducer.js +406 -0
- package/dist/agents/goal-store.js +35 -1
- package/dist/agents/mobile-flywheel-view.js +144 -0
- package/dist/agents/mobile-task-actions-verify.js +74 -0
- package/dist/agents/mobile-task-actions.js +178 -0
- package/dist/agents/mobile-task-views.js +199 -0
- package/dist/agents/model-policy.js +651 -0
- package/dist/agents/network-pulse.js +304 -18
- package/dist/agents/pi-sdk-tools.js +84 -33
- package/dist/agents/pi-sdk.js +157 -42
- package/dist/agents/run-store.js +71 -5
- package/dist/agents/skill-readiness.js +52 -10
- package/dist/agents/subagent-manager.js +209 -3
- package/dist/agents/task/task-runner.js +17 -4
- package/dist/agents/task-group.js +8 -51
- package/dist/agents/task-public-text.js +89 -0
- package/dist/agents/workflow-pivot-loop.js +19 -12
- package/dist/cli/clipboard.js +65 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/tasks.js +350 -2
- package/dist/cli/identity-command.js +177 -0
- package/dist/cli/ink-app.js +145 -17
- package/dist/cli/log-gate.js +386 -0
- package/dist/cli/mention-data.js +2 -0
- package/dist/cli/model-selector.js +538 -0
- package/dist/cli/reply-hygiene.js +54 -0
- package/dist/cli/setup-wizard.js +757 -69
- package/dist/cli/startup-notice.js +160 -0
- package/dist/cli/stores.js +22 -1
- package/dist/cli/theme.js +49 -0
- package/dist/cli/tui-select.js +970 -0
- package/dist/cli/update-commands.js +150 -24
- package/dist/cli-entry.js +137 -20
- package/dist/electron-build/agents/contacts/chain.js +26 -6
- package/dist/electron-build/agents/contacts/chain.js.map +1 -1
- package/dist/electron-build/agents/execution-supervisor.js +578 -38
- package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
- package/dist/electron-build/agents/external-events.js +34 -5
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-criteria.js +10 -3
- package/dist/electron-build/agents/goal-criteria.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
- package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
- package/dist/electron-build/agents/goal-state-reducer.js +414 -0
- package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
- package/dist/electron-build/agents/goal-store.js +35 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/model-policy.js +706 -0
- package/dist/electron-build/agents/model-policy.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +307 -19
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +156 -41
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +75 -6
- package/dist/electron-build/agents/run-store.js.map +1 -1
- package/dist/electron-build/agents/skill-readiness.js +52 -9
- package/dist/electron-build/agents/skill-readiness.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +16 -3
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/workflow-pivot-loop.js +19 -12
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/llm/config-store.js +146 -6
- package/dist/electron-build/llm/config-store.js.map +1 -1
- package/dist/electron-build/llm/connection-probe.js +764 -0
- package/dist/electron-build/llm/connection-probe.js.map +1 -0
- package/dist/electron-build/llm/custom-provider-store.js +272 -0
- package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
- package/dist/electron-build/llm/model-catalog.js +778 -0
- package/dist/electron-build/llm/model-catalog.js.map +1 -0
- package/dist/electron-build/llm/model-discovery.js +1202 -0
- package/dist/electron-build/llm/model-discovery.js.map +1 -0
- package/dist/electron-build/llm/model-selection.js +1315 -0
- package/dist/electron-build/llm/model-selection.js.map +1 -0
- package/dist/electron-build/llm/pi-ai.js +70 -11
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/llm/provider-catalog.js +969 -0
- package/dist/electron-build/llm/provider-catalog.js.map +1 -0
- package/dist/electron-build/llm/provider-registry.js +950 -0
- package/dist/electron-build/llm/provider-registry.js.map +1 -0
- package/dist/electron-build/llm/tool-name.js +266 -0
- package/dist/electron-build/llm/tool-name.js.map +1 -0
- package/dist/electron-build/orbitdb/cid-database.js +83 -17
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/utils/dual-source-facts.js +236 -0
- package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
- package/dist/electron-build/utils/dual-source.js +347 -0
- package/dist/electron-build/utils/dual-source.js.map +1 -0
- package/dist/electron-build/utils/update-manager.js +328 -104
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +9 -11
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-identity.js +193 -0
- package/dist/electron-build/utils/version-identity.js.map +1 -0
- package/dist/electron-build/utils/version-info.js +57 -31
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +385 -108
- package/dist/ios/bolloon-web.json +10 -0
- package/dist/ios/chain-explorer.js +617 -0
- package/dist/ios/explorer.html +163 -0
- package/dist/ios/index.html +96 -0
- package/dist/ios/mobile-agent.js +2 -2
- package/dist/ios/mobile-contacts.js +362 -0
- package/dist/ios/mobile-core.js +3102 -310
- package/dist/ios/mobile-privacy.js +185 -0
- package/dist/ios/mobile-tasks.js +339 -0
- package/dist/ios/mobile-update.js +1442 -0
- package/dist/ios/mobile.css +19 -0
- package/dist/ios/mobile.html +96 -0
- package/dist/ios/mobile.js +1013 -41
- package/dist/ios/routes-chain-index.js +108 -0
- package/dist/ios/routes-contacts.js +420 -0
- package/dist/ios/routes-llm-config.js +344 -52
- package/dist/ios/routes-mobile-tasks.js +353 -0
- package/dist/ios/routes-onchain-trade.js +99 -0
- package/dist/ios/routes-x402-info.js +1 -0
- package/dist/ios/server.js +921 -0
- package/dist/ios/sw.js +1 -1
- package/dist/ios/task-frame-server.js +211 -0
- package/dist/llm/config-store.js +146 -6
- package/dist/llm/connection-probe.js +744 -0
- package/dist/llm/custom-provider-store.js +224 -0
- package/dist/llm/data/provider-catalog-baked.js +19 -0
- package/dist/llm/model-catalog.js +717 -0
- package/dist/llm/model-discovery.js +1137 -0
- package/dist/llm/model-selection.js +1241 -0
- package/dist/llm/pi-ai.js +70 -11
- package/dist/llm/provider-catalog.js +898 -0
- package/dist/llm/provider-registry.js +917 -0
- package/dist/llm/tool-name.js +220 -0
- package/dist/orbitdb/cid-database.js +81 -16
- package/dist/orbitdb/ipfs-node.js +31 -2
- package/dist/setup/onboard.js +34 -8
- package/dist/utils/dual-source-facts.js +223 -0
- package/dist/utils/dual-source.js +297 -0
- package/dist/utils/update-cli.js +16 -21
- package/dist/utils/update-health.js +35 -5
- package/dist/utils/update-manager.js +325 -96
- package/dist/utils/update-state.js +13 -10
- package/dist/utils/version-identity.js +176 -0
- package/dist/utils/version-info.js +27 -22
- package/dist/web/bolloon-web.json +10 -0
- package/dist/web/mobile-contacts.js +44 -11
- package/dist/web/mobile-core.js +2901 -580
- package/dist/web/mobile-tasks.js +339 -0
- package/dist/web/mobile-update.js +1442 -0
- package/dist/web/mobile.css +19 -0
- package/dist/web/mobile.html +64 -0
- package/dist/web/mobile.js +838 -35
- package/dist/web/routes-llm-config.js +344 -52
- package/dist/web/routes-mobile-tasks.js +353 -0
- package/dist/web/server.js +163 -4
- package/dist/web/sw.js +1 -1
- package/package.json +3 -1
- package/scripts/build-web.ts +36 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goal-flywheel/index.ts — 「Goal 长期执行飞轮」对外入口 (2026-09-25)
|
|
3
|
+
*
|
|
4
|
+
* 冻结的接口类型在 `types.ts`; P0–P4 的实现逐个从本入口转发 (接线是单一所有者的事,
|
|
5
|
+
* 这里的转发面 = 接线层真正用到的那些):
|
|
6
|
+
* P0 节奏由进展决定 → continuation-decision.ts
|
|
7
|
+
* P1 强制收尾飞轮 → run-closure.ts
|
|
8
|
+
* P1b Memory / Skill 候选 → memory-layers.ts · skill-candidate.ts
|
|
9
|
+
* P2 子 Agent 工作合同 → work-contract.ts
|
|
10
|
+
* P3 阻塞监控 → work-monitor.ts
|
|
11
|
+
* P4 新要求注入 → goal-change.ts
|
|
12
|
+
*
|
|
13
|
+
* 真实调用路径的适配层 (`src/agents/goal-flywheel-wiring.ts`) **不在这里** —— 它 import
|
|
14
|
+
* goal-store / run-store, 属于"接线 = 改现有调用方"那一层, 放在目录外面只 import, 不侵入。
|
|
15
|
+
*
|
|
16
|
+
* 本文件只做转发: 不夹带实现、不定义函数 (由 `goal-flywheel-types.test.ts` 的门钉住)。
|
|
17
|
+
* 类型一律用内联 `type` 修饰符跟在值后面转发 —— 与本目录既有的"转发行写法"门保持一致。
|
|
18
|
+
*/
|
|
19
|
+
export * from './types.js';
|
|
20
|
+
export { applyHardLimits, decideContinuation, isRunnable, } from './continuation-decision.js';
|
|
21
|
+
export { CLOSURE_DEFAULT_NO_PROGRESS_CIRCUIT_BREAKER, CLOSURE_MAX_EVIDENCE, CLOSURE_MAX_FACTS, CLOSURE_MIN_PURPOSE_LENGTH, CLOSURE_RESULT_FIELDS, CLOSURE_STEP_ORDER, RunClosureError, closeRun, gateClosureCandidate, parseFinalReview, } from './run-closure.js';
|
|
22
|
+
export { MEMORY_LAYERS_ROOT, MEMORY_REJECT_REASONS, expireTemporary, memoryFileName, memoryFilePath, validateMemoryRecordForWrite, writeMemoryRecords, } from './memory-layers.js';
|
|
23
|
+
export { TEMP_PATH_HINTS, assessCandidate, bumpSkillVersion, deriveJunkReasons, draftPromotion, looksLikeTempPath, } from './skill-candidate.js';
|
|
24
|
+
export { CRITERION_REF_PREFIX, DEFAULT_CANCEL_POLICY, DEFAULT_FAILURE_POLICY, DEFAULT_HEARTBEAT_INTERVAL_MS, MAX_HEARTBEAT_INTERVAL_MS, MIN_HEARTBEAT_INTERVAL_MS, NOT_CHECKABLE_FROM_REPORT, REPORT_CHECKABLE_PROHIBITIONS, WORK_REPORT_SCHEMA, WorkContractError, acceptsAsComplete, criterionRef, issueWorkContract, stableHash, validateChildReport, } from './work-contract.js';
|
|
25
|
+
export { ADJUSTMENT_ALREADY_GIVEN_MULTIPLE, BLOCK_HANDLING_DEFAULTS, DEFAULT_ESCALATION_MS, DEFAULT_REPORT_GRACE_MS, HEARTBEAT_MISS_MULTIPLE, NO_PROGRESS_FALLBACK_MS, NO_PROGRESS_HEARTBEAT_MULTIPLE, REPORT_GRACE_HEARTBEAT_MULTIPLE, TERMINAL_GOAL_STATES, USER_VISIBLE_BLOCK_PRIORITY, USER_VISIBLE_STATE_FOR_BLOCK_KIND, detectBlocks, heartbeatWindowMs, missingReportParts, noProgressWindowMs, planBlockHandling, reportGraceMs, toUserVisibleState, } from './work-monitor.js';
|
|
26
|
+
export { applyChange, approvalAsUserChange, childChangeDirective, classifyChange, detectBudgetDirection, detectChangeIntents, ingestChange, isWidening, nextStatusFor, scopeChangeToWork, shouldSupersedePending, } from './goal-change.js';
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-layers.ts — P1b: Memory 分层 (落盘 + 过期归档) (2026-09-25)
|
|
3
|
+
*
|
|
4
|
+
* 归属: `docs/wiki/goal-continuation-flywheel.md` §13「P1b Memory/Skill」独占文件。
|
|
5
|
+
* 本文件**只**实现 §14 冻结签名里的 `writeMemoryRecords` / `expireTemporary`, 不碰任何现有调用方,
|
|
6
|
+
* 也不 import 其它阶段的实现 (§13: 阶段之间只通过 `types.ts` 的类型耦合)。
|
|
7
|
+
*
|
|
8
|
+
* 分层语义 (types.ts §3 的类型在**写入时**也要被真的执行, 不能只写在类型上):
|
|
9
|
+
*
|
|
10
|
+
* | 层 | 落盘规则 |
|
|
11
|
+
* | --- | --- |
|
|
12
|
+
* | `run_fact` | `assertion='confirmed'` 必须有 `confirmedRefs` (推测永不当证据); `assertion='inferred'` 不许带 `confirmedRefs` |
|
|
13
|
+
* | `lesson` | 必须有 `reviewedBy`, 且 `reviewVerdict='reusable'` —— `one_off` **不落盘** |
|
|
14
|
+
* | `decision` | `sourceRef` 必填 (涉用户偏好/重要取舍必须保留来源) |
|
|
15
|
+
* | `skill_signal` | `promotesDirectly` 必须为 `false` (类型级陈述在运行期也要拦) |
|
|
16
|
+
* | `temporary` | `expiresAt` 合法且 **晚于 now**; 已过期/不合法 → 拒绝 (不许写入即死数据) |
|
|
17
|
+
*
|
|
18
|
+
* 落盘布局 (与 `goal-store.ts` 同一手法: 每条例行一个 JSON, 原子写 tmp+rename):
|
|
19
|
+
* `<home>/.bolloon/memory-layers/<layer>/<memoryId 转义>.json`
|
|
20
|
+
* —— `home` 由调用方注入 (便于测试), 不读真实 `os.homedir()`。
|
|
21
|
+
*
|
|
22
|
+
* 幂等: 同一 `memoryId` 且内容逐字相同 → 视为已写入 (不重复落盘, 计入 `written`);
|
|
23
|
+
* 同一 `memoryId` 但内容不同 → 拒绝 (`duplicate_memory_id`), **绝不覆盖**已有记忆。
|
|
24
|
+
*
|
|
25
|
+
* 阴性对照 (怎么知道这些门不是空转): 把 `assertion='confirmed'` 的来源检查删掉 →
|
|
26
|
+
* 「confirmed 无来源必须被拒」立刻判红; 把 `expireTemporary` 的 `> now` 改成「永不归档」→
|
|
27
|
+
* 「到期必须归档」立刻判红。见 `src/test/goal-flywheel-memory.test.ts`。
|
|
28
|
+
*/
|
|
29
|
+
import * as fs from 'node:fs/promises';
|
|
30
|
+
import * as path from 'node:path';
|
|
31
|
+
import { MEMORY_LAYERS } from './types.js';
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// 对外常量 (结构化原因 / 布局)
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
/** 记忆落盘根 (相对注入的 `home`) */
|
|
36
|
+
export const MEMORY_LAYERS_ROOT = '.bolloon/memory-layers';
|
|
37
|
+
/** 写入被拒的结构化原因 (不许只返回 boolean) */
|
|
38
|
+
export const MEMORY_REJECT_REASONS = [
|
|
39
|
+
'invalid_memory_id', // memoryId 为空
|
|
40
|
+
'unknown_layer', // layer 不在 MEMORY_LAYERS 里
|
|
41
|
+
'empty_content', // 内容为空 (空记忆没有意义)
|
|
42
|
+
'unknown_assertion', // assertion 不是 confirmed/inferred
|
|
43
|
+
'confirmed_fact_without_source', // confirmed 事实没有来源 (推测当证据)
|
|
44
|
+
'inferred_fact_cannot_confirm', // inferred 事实带了 confirmedRefs (混淆确定性)
|
|
45
|
+
'lesson_without_reviewer', // lesson 没有 reviewedBy
|
|
46
|
+
'lesson_not_reusable', // lesson 的 reviewVerdict 不是 reusable (one_off)
|
|
47
|
+
'decision_without_source', // decision 没有 sourceRef
|
|
48
|
+
'skill_signal_cannot_promote', // skill_signal.promotesDirectly 不是 false
|
|
49
|
+
'skill_signal_without_candidate', // skill_signal 没有 candidateId
|
|
50
|
+
'temporary_without_valid_expiry', // temporary 的 expiresAt 不合法 / archiveAtRunEnd 不是 true
|
|
51
|
+
'temporary_already_expired', // 写入时已经过期 (写进去就是死数据)
|
|
52
|
+
'duplicate_memory_id', // 同 id 已存在且内容不同 (不许覆盖)
|
|
53
|
+
];
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// 小工具 (纯)
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
const NON_EMPTY = (v) => typeof v === 'string' && v.trim().length > 0;
|
|
58
|
+
/** 是否是合法 ISO 时间戳 (本仓时间一律注入, 且必须是能解析的 ISO) */
|
|
59
|
+
function isValidIso(v) {
|
|
60
|
+
if (typeof v !== 'string' || v.trim().length === 0)
|
|
61
|
+
return false;
|
|
62
|
+
if (!/^\d{4}-\d{2}-\d{2}T/.test(v))
|
|
63
|
+
return false;
|
|
64
|
+
return Number.isFinite(Date.parse(v));
|
|
65
|
+
}
|
|
66
|
+
function assertNow(now, who) {
|
|
67
|
+
if (!isValidIso(now)) {
|
|
68
|
+
throw new Error(`${who}: now 必须是合法 ISO 时间戳 (收到 ${JSON.stringify(now)})`);
|
|
69
|
+
}
|
|
70
|
+
return Date.parse(now);
|
|
71
|
+
}
|
|
72
|
+
/** memoryId → 文件名 (Windows 文件名不含 `:`; 见 AGENTS.md §5.1) */
|
|
73
|
+
export function memoryFileName(memoryId) {
|
|
74
|
+
const safe = String(memoryId).replace(/[^A-Za-z0-9._-]/g, '_');
|
|
75
|
+
return `${safe.length > 0 ? safe : 'unnamed'}.json`;
|
|
76
|
+
}
|
|
77
|
+
/** 一条记忆的落盘路径 (导出给调用方/测试核对, 避免两处各手抄一份布局) */
|
|
78
|
+
export function memoryFilePath(home, record) {
|
|
79
|
+
return path.join(home, MEMORY_LAYERS_ROOT, record.layer, memoryFileName(record.memoryId));
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// 写入时的分层校验
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
/** 校验一条记忆能不能落盘; 通过返回 null, 否则返回结构化原因 */
|
|
85
|
+
export function validateMemoryRecordForWrite(record, now) {
|
|
86
|
+
const nowMs = assertNow(now, 'validateMemoryRecordForWrite');
|
|
87
|
+
if (!NON_EMPTY(record.memoryId))
|
|
88
|
+
return 'invalid_memory_id';
|
|
89
|
+
if (!MEMORY_LAYERS.includes(record.layer))
|
|
90
|
+
return 'unknown_layer';
|
|
91
|
+
if (!NON_EMPTY(record.content))
|
|
92
|
+
return 'empty_content';
|
|
93
|
+
switch (record.layer) {
|
|
94
|
+
case 'run_fact': {
|
|
95
|
+
const confirmed = (record.confirmedRefs ?? []).filter(NON_EMPTY);
|
|
96
|
+
if (record.assertion === 'confirmed') {
|
|
97
|
+
if (confirmed.length === 0)
|
|
98
|
+
return 'confirmed_fact_without_source';
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
if (record.assertion === 'inferred') {
|
|
102
|
+
// 推测永远不允许携带"已确认来源" (两者互斥)
|
|
103
|
+
if (confirmed.length > 0)
|
|
104
|
+
return 'inferred_fact_cannot_confirm';
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return 'unknown_assertion';
|
|
108
|
+
}
|
|
109
|
+
case 'lesson': {
|
|
110
|
+
if (!NON_EMPTY(record.reviewedBy))
|
|
111
|
+
return 'lesson_without_reviewer';
|
|
112
|
+
if (record.reviewVerdict !== 'reusable')
|
|
113
|
+
return 'lesson_not_reusable';
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
case 'decision': {
|
|
117
|
+
if (!NON_EMPTY(record.sourceRef))
|
|
118
|
+
return 'decision_without_source';
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
case 'skill_signal': {
|
|
122
|
+
// 类型上是字面量 false; 运行期也要拦 (别让 `as any` 把转正权带进来)
|
|
123
|
+
if (record.promotesDirectly !== false)
|
|
124
|
+
return 'skill_signal_cannot_promote';
|
|
125
|
+
if (!NON_EMPTY(record.candidateId))
|
|
126
|
+
return 'skill_signal_without_candidate';
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
case 'temporary': {
|
|
130
|
+
if (record.archiveAtRunEnd !== true)
|
|
131
|
+
return 'temporary_without_valid_expiry';
|
|
132
|
+
if (!isValidIso(record.expiresAt))
|
|
133
|
+
return 'temporary_without_valid_expiry';
|
|
134
|
+
if (Date.parse(record.expiresAt) <= nowMs)
|
|
135
|
+
return 'temporary_already_expired';
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// §14 冻结签名 ①: writeMemoryRecords
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
/**
|
|
144
|
+
* 把一批分层记忆写入 `<home>/.bolloon/memory-layers/`。
|
|
145
|
+
*
|
|
146
|
+
* 逐条独立判定: 一条被拒不影响同批其它条 (返回里 `written` + `rejected` 恰好等于输入条数)。
|
|
147
|
+
* 被拒的记录**不落地任何文件** (不许"先写坏再回滚")。
|
|
148
|
+
*/
|
|
149
|
+
export async function writeMemoryRecords(home, records, now) {
|
|
150
|
+
if (!NON_EMPTY(home))
|
|
151
|
+
throw new Error('writeMemoryRecords: home 必填 (调用方注入, 不许猜)');
|
|
152
|
+
assertNow(now, 'writeMemoryRecords');
|
|
153
|
+
const written = [];
|
|
154
|
+
const rejected = [];
|
|
155
|
+
const inBatch = new Set();
|
|
156
|
+
for (const record of records) {
|
|
157
|
+
const id = typeof record?.memoryId === 'string' ? record.memoryId : '';
|
|
158
|
+
const reason = validateMemoryRecordForWrite(record, now);
|
|
159
|
+
if (reason !== null) {
|
|
160
|
+
rejected.push({ memoryId: id, reason });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (inBatch.has(id)) {
|
|
164
|
+
rejected.push({ memoryId: id, reason: 'duplicate_memory_id' });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const filePath = memoryFilePath(home, record);
|
|
168
|
+
const payload = JSON.stringify(record, null, 2);
|
|
169
|
+
let existing = null;
|
|
170
|
+
try {
|
|
171
|
+
existing = await fs.readFile(filePath, 'utf8');
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
existing = null;
|
|
175
|
+
}
|
|
176
|
+
if (existing !== null) {
|
|
177
|
+
// 同 id 且逐字相同 → 幂等 (不重复落盘); 内容不同 → 拒绝, 绝不覆盖
|
|
178
|
+
if (existing.trim() === payload.trim()) {
|
|
179
|
+
inBatch.add(id);
|
|
180
|
+
written.push(id);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
rejected.push({ memoryId: id, reason: 'duplicate_memory_id' });
|
|
184
|
+
}
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
188
|
+
const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
189
|
+
await fs.writeFile(tmp, payload, 'utf8');
|
|
190
|
+
await fs.rename(tmp, filePath);
|
|
191
|
+
inBatch.add(id);
|
|
192
|
+
written.push(id);
|
|
193
|
+
}
|
|
194
|
+
return { written, rejected };
|
|
195
|
+
}
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// §14 冻结签名 ②: expireTemporary (纯函数, 不碰 I/O)
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
/**
|
|
200
|
+
* 按时间把 `temporary` 层分成"留"与"归档"。**纯函数** (零 I/O, 不改入参, 保序)。
|
|
201
|
+
*
|
|
202
|
+
* 只有同时满足三条的 temporary 才留:
|
|
203
|
+
* ① `archiveAtRunEnd === true` (类型级陈述在运行期复核)
|
|
204
|
+
* ② `expiresAt` 是合法 ISO
|
|
205
|
+
* ③ `expiresAt > now` (**到点即归档**, 边界取等号算过期)
|
|
206
|
+
* 其余 temporary 一律归档 —— 拿不出"未过期"证据的临时记忆不许长期留着。
|
|
207
|
+
* 非 temporary 的记录**永不**被归档 (哪怕字段看起来过期)。
|
|
208
|
+
*/
|
|
209
|
+
export function expireTemporary(records, now) {
|
|
210
|
+
const nowMs = assertNow(now, 'expireTemporary');
|
|
211
|
+
const kept = [];
|
|
212
|
+
const archived = [];
|
|
213
|
+
for (const record of records) {
|
|
214
|
+
if (record.layer !== 'temporary') {
|
|
215
|
+
kept.push(record);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const live = record.archiveAtRunEnd === true &&
|
|
219
|
+
isValidIso(record.expiresAt) &&
|
|
220
|
+
Date.parse(record.expiresAt) > nowMs;
|
|
221
|
+
if (live)
|
|
222
|
+
kept.push(record);
|
|
223
|
+
else
|
|
224
|
+
archived.push(record);
|
|
225
|
+
}
|
|
226
|
+
return { kept, archived };
|
|
227
|
+
}
|