@bolloon/bolloon-agent 0.4.32 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/chain/explorer.js +52 -0
- package/dist/agents/chain/index.js +2 -0
- package/dist/agents/chain/onchain-trade.js +15 -1
- package/dist/agents/contacts/chain.js +27 -7
- package/dist/agents/execution-supervisor.js +493 -35
- 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/network-pulse.js +525 -27
- package/dist/agents/pi-sdk.js +75 -20
- package/dist/agents/run-store.js +18 -5
- package/dist/agents/skill-readiness.js +52 -10
- package/dist/agents/subagent-manager.js +209 -3
- package/dist/agents/task/task-onchain-runner.js +19 -1
- package/dist/agents/task/task-runner.js +36 -6
- package/dist/agents/task-board.js +702 -0
- package/dist/agents/task-group.js +524 -0
- package/dist/agents/task-public-text.js +89 -0
- package/dist/cli/commands/index.js +3 -0
- package/dist/cli/commands/tasks.js +977 -8
- package/dist/cli/identity-command.js +177 -0
- package/dist/cli/protocol-envelope.js +4 -0
- package/dist/cli/setup-wizard.js +71 -0
- package/dist/cli/update-commands.js +150 -24
- package/dist/cli-entry.js +12 -2
- package/dist/electron-build/agents/chain/explorer.js +59 -0
- package/dist/electron-build/agents/chain/explorer.js.map +1 -0
- 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 +492 -34
- 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/local-signer.js +101 -0
- package/dist/electron-build/agents/local-signer.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +531 -28
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +74 -19
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +20 -5
- 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 +35 -5
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/task-board.js +761 -0
- package/dist/electron-build/agents/task-board.js.map +1 -0
- package/dist/electron-build/agents/task-contract.js +382 -0
- package/dist/electron-build/agents/task-contract.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.js +471 -0
- package/dist/electron-build/utils/dual-source.js.map +1 -0
- package/dist/electron-build/utils/update-manager.js +331 -57
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +16 -1
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-info.js +82 -2
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +103 -12
- 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 +329 -0
- package/dist/ios/mobile-core.js +1379 -226
- package/dist/ios/mobile-privacy.js +185 -0
- package/dist/ios/mobile.css +19 -0
- package/dist/ios/mobile.html +96 -0
- package/dist/ios/mobile.js +706 -7
- package/dist/ios/routes-chain-index.js +108 -0
- package/dist/ios/routes-contacts.js +420 -0
- package/dist/ios/routes-onchain-trade.js +99 -0
- package/dist/ios/routes-x402-info.js +1 -0
- package/dist/ios/server.js +762 -0
- package/dist/ios/task-frame-server.js +211 -0
- package/dist/orbitdb/cid-database.js +81 -16
- package/dist/orbitdb/ipfs-node.js +31 -2
- package/dist/utils/dual-source.js +426 -0
- package/dist/utils/update-cli.js +16 -21
- package/dist/utils/update-health.js +35 -5
- package/dist/utils/update-manager.js +329 -57
- package/dist/utils/update-state.js +16 -1
- package/dist/utils/version-info.js +75 -1
- package/dist/web/mobile-contacts.js +44 -11
- package/dist/web/mobile-core.js +1197 -513
- package/dist/web/mobile-tasks.js +339 -0
- package/dist/web/mobile.css +19 -0
- package/dist/web/mobile.html +64 -0
- package/dist/web/mobile.js +531 -1
- package/dist/web/routes-mobile-tasks.js +353 -0
- package/dist/web/server.js +163 -4
- package/package.json +3 -1
|
@@ -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
|
+
}
|