@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
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/README.md +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
package/dist/src/state-ledger.js
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 事务化状态账本(ADR-15,docs/rfc/transactional-state-rfc.md):
|
|
3
|
+
* 「文件即权威」→「单文件 SQLite 账本即权威」。
|
|
4
|
+
*
|
|
5
|
+
* - events 表 = 唯一权威(append-only;语义幂等键物理化为 UNIQUE 索引)
|
|
6
|
+
* - 投影表(projection_docs/items)= 日志的确定性 fold,可随时 DROP 重建
|
|
7
|
+
* - 红线进事务:evidence/conditions 校验发生在写事务内,拒绝即回滚账本无痕
|
|
8
|
+
* - 一次写 = 单事务{INSERT event; UPDATE 投影},跨文件分叉在物理上不可能
|
|
9
|
+
* - 语义层零改造:mergeProfile/appendTrip/upsertCompanion/appendEvent 纯函数
|
|
10
|
+
* 原样复用为写路径守门与 fold 处理器(memory-design §1.6「账本化只换存储面」)
|
|
11
|
+
*
|
|
12
|
+
* 迁移纪律(D2 one-shot):旧 JSON/JSONL 文件在首次写时自动导入为 events
|
|
13
|
+
* (单事务 + 导入前快照到 gotry-state/pre-ledger-backup/);此后文件降级为
|
|
14
|
+
* 导出视图(state-cli export),永不再回流。读路径带文件回退(未迁移 root
|
|
15
|
+
* 仍可读旧文件),写路径必经账本——工具外无写入路径。
|
|
16
|
+
*
|
|
17
|
+
* 引擎:better-sqlite3(复用矩阵 open-source import 通道),WAL + NORMAL,
|
|
18
|
+
* 同步 API 与仓内 readFileSync 风格一致;busy_timeout 容忍跨进程
|
|
19
|
+
* (dsh 运行时 / async-collect / state-cli tick 并存)。
|
|
20
|
+
*/
|
|
1
21
|
import Database from 'better-sqlite3';
|
|
2
22
|
import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
23
|
import { createHash } from 'node:crypto';
|
|
@@ -6,13 +26,15 @@ import { mergeProfile } from './memory-capture.js';
|
|
|
6
26
|
import { appendEvent as gateUtilityEvent, projectUtility } from './memory-utility.js';
|
|
7
27
|
import { appendTrip as gateTrip } from './travel-timeline.js';
|
|
8
28
|
import { upsertCompanion } from './companions.js';
|
|
9
|
-
const SCHEMA_VERSION = '
|
|
29
|
+
const SCHEMA_VERSION = '2';
|
|
30
|
+
/** 稳定 wish 主键:名称语义派生(重放/同名幂等更新不再依赖墙钟;RFC §1.1 id 不稳定缺口的修复) */
|
|
10
31
|
export function makeWishId(name) {
|
|
11
32
|
return 'w-' + createHash('sha256').update(String(name).trim()).digest('hex').slice(0, 10);
|
|
12
33
|
}
|
|
13
34
|
function sha(s) {
|
|
14
35
|
return createHash('sha256').update(s).digest('hex');
|
|
15
36
|
}
|
|
37
|
+
/** stateRoot 可相对可绝对(与 incident-log.resolveIncidentsPath 同款解析) */
|
|
16
38
|
function stateDirOf(stateRoot) {
|
|
17
39
|
const root = isAbsolute(stateRoot) ? stateRoot : join(process.cwd(), stateRoot);
|
|
18
40
|
return join(root, 'gotry-state');
|
|
@@ -23,16 +45,10 @@ export function ledgerDbPath(stateRoot) {
|
|
|
23
45
|
export function ledgerExists(stateRoot) {
|
|
24
46
|
return existsSync(ledgerDbPath(stateRoot));
|
|
25
47
|
}
|
|
26
|
-
const LEGACY_FILES = [
|
|
27
|
-
'motivation-profile.json',
|
|
28
|
-
'wish-pool.json',
|
|
29
|
-
'memory-utility.jsonl',
|
|
30
|
-
'trips.jsonl',
|
|
31
|
-
'companions.json'
|
|
32
|
-
];
|
|
48
|
+
const LEGACY_FILES = ['motivation-profile.json', 'wish-pool.json', 'memory-utility.jsonl', 'trips.jsonl', 'companions.json'];
|
|
33
49
|
function legacyFilesPresent(stateRoot) {
|
|
34
50
|
const dir = stateDirOf(stateRoot);
|
|
35
|
-
return LEGACY_FILES.some(
|
|
51
|
+
return LEGACY_FILES.some(f => existsSync(join(dir, f)));
|
|
36
52
|
}
|
|
37
53
|
function isUniqueViolation(e) {
|
|
38
54
|
return e instanceof Error && e.code === 'SQLITE_CONSTRAINT_UNIQUE';
|
|
@@ -108,101 +124,86 @@ export class StateLedger {
|
|
|
108
124
|
stateRoot;
|
|
109
125
|
tenant;
|
|
110
126
|
dbPath;
|
|
111
|
-
constructor(db, stateRoot, tenant = 'local'){
|
|
127
|
+
constructor(db, stateRoot, tenant = 'local') {
|
|
112
128
|
this.db = db;
|
|
113
129
|
this.stateRoot = stateRoot;
|
|
114
130
|
this.tenant = tenant;
|
|
115
131
|
this.dbPath = ledgerDbPath(stateRoot);
|
|
116
132
|
}
|
|
133
|
+
// ---- 基础事件面 ------------------------------------------------------------
|
|
134
|
+
/** 事件插入(idem 命中 → 返回 null,不抛;红线校验异常向上抛 = 事务回滚) */
|
|
117
135
|
insertEvent(ev) {
|
|
118
|
-
const stmt = this.db.prepare(`INSERT INTO events (ts, actor, kind, subject_id, payload, idem_key, run_id)
|
|
119
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
136
|
+
const stmt = this.db.prepare(`INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id)
|
|
137
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
120
138
|
try {
|
|
121
|
-
const info = stmt.run(ev.ts ?? new Date().toISOString(), ev.actor, ev.kind, ev.subjectId ?? '', JSON.stringify(ev.payload), ev.idemKey ?? null, ev.runId ?? null);
|
|
139
|
+
const info = stmt.run(this.tenant, ev.ts ?? new Date().toISOString(), ev.actor, ev.kind, ev.subjectId ?? '', JSON.stringify(ev.payload), ev.idemKey ?? null, ev.runId ?? null);
|
|
122
140
|
return Number(info.lastInsertRowid);
|
|
123
|
-
}
|
|
124
|
-
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
if (isUniqueViolation(e))
|
|
144
|
+
return null;
|
|
125
145
|
throw e;
|
|
126
146
|
}
|
|
127
147
|
}
|
|
128
148
|
readEvents(kind, limit = 100) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
kind,
|
|
133
|
-
|
|
134
|
-
] : [
|
|
135
|
-
limit
|
|
136
|
-
]);
|
|
149
|
+
const rows = kind
|
|
150
|
+
? this.db.prepare(`SELECT seq, tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id
|
|
151
|
+
FROM events WHERE tenant_id = ? AND kind = ? ORDER BY seq DESC LIMIT ?`).all(this.tenant, kind, limit)
|
|
152
|
+
: this.db.prepare(`SELECT seq, tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id
|
|
153
|
+
FROM events WHERE tenant_id = ? ORDER BY seq DESC LIMIT ?`).all(this.tenant, limit);
|
|
137
154
|
return rows;
|
|
138
155
|
}
|
|
139
156
|
countEvents() {
|
|
140
157
|
return this.db.prepare('SELECT COUNT(*) AS n FROM events WHERE tenant_id = ?').get(this.tenant).n;
|
|
141
158
|
}
|
|
159
|
+
// ---- 读:投影与日志类事件 ----------------------------------------------------
|
|
142
160
|
readMotivation() {
|
|
143
161
|
const row = this.db.prepare(`SELECT doc FROM projection_docs WHERE subject = 'motivation' AND tenant_id = ?`).get(this.tenant);
|
|
144
162
|
return row ? JSON.parse(row.doc) : null;
|
|
145
163
|
}
|
|
146
164
|
readWishPool() {
|
|
147
165
|
const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' AND tenant_id = ? ORDER BY ord, rowid`).all(this.tenant);
|
|
148
|
-
return rows.map(
|
|
166
|
+
return rows.map(r => JSON.parse(r.doc));
|
|
149
167
|
}
|
|
150
168
|
readCompanions() {
|
|
151
169
|
const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'companions' AND tenant_id = ? ORDER BY ord, rowid`).all(this.tenant);
|
|
152
|
-
return rows.map(
|
|
170
|
+
return rows.map(r => JSON.parse(r.doc));
|
|
153
171
|
}
|
|
154
172
|
readUtilityEvents() {
|
|
155
173
|
const rows = this.db.prepare(`SELECT payload FROM events WHERE kind = 'memory_utility.event' AND tenant_id = ? ORDER BY seq`).all(this.tenant);
|
|
156
|
-
return rows.map(
|
|
174
|
+
return rows.map(r => JSON.parse(r.payload).event);
|
|
157
175
|
}
|
|
158
176
|
readTrips() {
|
|
159
177
|
const rows = this.db.prepare(`SELECT payload FROM events WHERE kind = 'trip.logged' AND tenant_id = ? ORDER BY seq`).all(this.tenant);
|
|
160
|
-
return rows.map(
|
|
178
|
+
return rows.map(r => JSON.parse(r.payload).trip);
|
|
161
179
|
}
|
|
180
|
+
/** 效用投影(与文件版 projectUtility 同源同构) */
|
|
162
181
|
projectUtilityNow() {
|
|
163
182
|
return projectUtility(this.readUtilityEvents());
|
|
164
183
|
}
|
|
184
|
+
// ---- 写:单事务{INSERT event; UPDATE 投影},守门纯函数原样复用 -----------------
|
|
185
|
+
// 红线进事务:evidence 缺失/conditions 空 → mergeProfile/校验拒绝 → 事务回滚,账本无痕。
|
|
186
|
+
/** 动机画像增量补丁(守门 = mergeProfile:追加不删史/幂等/权重变更须伴新证据) */
|
|
165
187
|
appendMotivationPatch(patch, actor = 'tool:gotry_motivation_save') {
|
|
188
|
+
// 事件边界 ISO:事件行的 ts 就是 homeCityPreference.updated_at 的源头,
|
|
189
|
+
// fold 路径会读同一字段回放——rebuildProjections 必须能复现该值。
|
|
166
190
|
const ts = new Date().toISOString();
|
|
167
|
-
const run = this.db.transaction(()=>{
|
|
191
|
+
const run = this.db.transaction(() => {
|
|
168
192
|
const current = this.readMotivation();
|
|
169
|
-
const merged = mergeProfile(current, patch);
|
|
193
|
+
const merged = mergeProfile(current, patch, ts);
|
|
170
194
|
if (!merged) {
|
|
171
|
-
const unchanged = {
|
|
172
|
-
|
|
173
|
-
weights: {},
|
|
174
|
-
evidence: [],
|
|
175
|
-
hard: {}
|
|
176
|
-
},
|
|
177
|
-
updated_at: current?.updated_at ?? ts
|
|
178
|
-
};
|
|
179
|
-
return {
|
|
180
|
-
saved: false,
|
|
181
|
-
profile: unchanged
|
|
182
|
-
};
|
|
195
|
+
const unchanged = { ...(current ?? { weights: {}, evidence: [], hard: {} }), updated_at: current?.updated_at ?? ts };
|
|
196
|
+
return { saved: false, profile: unchanged };
|
|
183
197
|
}
|
|
184
|
-
const doc = {
|
|
185
|
-
|
|
186
|
-
updated_at: ts
|
|
187
|
-
};
|
|
188
|
-
this.insertEvent({
|
|
189
|
-
actor,
|
|
190
|
-
kind: 'motivation.patch',
|
|
191
|
-
subjectId: 'motivation',
|
|
192
|
-
payload: {
|
|
193
|
-
patch
|
|
194
|
-
},
|
|
195
|
-
ts
|
|
196
|
-
});
|
|
198
|
+
const doc = { ...merged, updated_at: ts };
|
|
199
|
+
this.insertEvent({ actor, kind: 'motivation.patch', subjectId: 'motivation', payload: { patch }, ts });
|
|
197
200
|
this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
|
|
198
201
|
ON CONFLICT(tenant_id, subject) DO UPDATE SET doc = excluded.doc`).run(this.tenant, JSON.stringify(doc));
|
|
199
|
-
return {
|
|
200
|
-
saved: true,
|
|
201
|
-
profile: doc
|
|
202
|
-
};
|
|
202
|
+
return { saved: true, profile: doc };
|
|
203
203
|
});
|
|
204
204
|
return run();
|
|
205
205
|
}
|
|
206
|
+
/** 愿望入池/同名幂等更新(红线:conditions 必填非空) */
|
|
206
207
|
appendWish(entry, actor = 'tool:gotry_wish_pool_add') {
|
|
207
208
|
const name = String(entry.name ?? '').trim();
|
|
208
209
|
const conditions = entry.conditions;
|
|
@@ -210,9 +211,9 @@ export class StateLedger {
|
|
|
210
211
|
throw new Error('wish pool entry requires name and conditions (fulfilment conditions are the whole point)');
|
|
211
212
|
}
|
|
212
213
|
const ts = new Date().toISOString();
|
|
213
|
-
const run = this.db.transaction(()=>{
|
|
214
|
+
const run = this.db.transaction(() => {
|
|
214
215
|
const pool = this.readWishPool();
|
|
215
|
-
const existing = pool.find(
|
|
216
|
+
const existing = pool.find(w => String(w.name ?? '') === name);
|
|
216
217
|
if (existing) {
|
|
217
218
|
const id = typeof existing.wish_id === 'string' && existing.wish_id ? existing.wish_id : makeWishId(name);
|
|
218
219
|
const next = {
|
|
@@ -220,32 +221,11 @@ export class StateLedger {
|
|
|
220
221
|
wish_id: id,
|
|
221
222
|
reason: entry.reason !== undefined ? entry.reason : existing.reason,
|
|
222
223
|
conditions,
|
|
223
|
-
...entry.muted !== undefined ? {
|
|
224
|
-
muted: Boolean(entry.muted)
|
|
225
|
-
} : {}
|
|
224
|
+
...(entry.muted !== undefined ? { muted: Boolean(entry.muted) } : {}),
|
|
226
225
|
};
|
|
227
|
-
this.insertEvent({
|
|
228
|
-
actor,
|
|
229
|
-
kind: 'wish.updated',
|
|
230
|
-
subjectId: id,
|
|
231
|
-
payload: {
|
|
232
|
-
wish_id: id,
|
|
233
|
-
patch: {
|
|
234
|
-
reason: next.reason,
|
|
235
|
-
conditions,
|
|
236
|
-
...entry.muted !== undefined ? {
|
|
237
|
-
muted: Boolean(entry.muted)
|
|
238
|
-
} : {}
|
|
239
|
-
}
|
|
240
|
-
},
|
|
241
|
-
ts
|
|
242
|
-
});
|
|
226
|
+
this.insertEvent({ actor, kind: 'wish.updated', subjectId: id, payload: { wish_id: id, patch: { reason: next.reason, conditions, ...(entry.muted !== undefined ? { muted: Boolean(entry.muted) } : {}) } }, ts });
|
|
243
227
|
this.upsertItem('wish_pool', id, next);
|
|
244
|
-
return {
|
|
245
|
-
added: false,
|
|
246
|
-
wish_id: id,
|
|
247
|
-
total: pool.length
|
|
248
|
-
};
|
|
228
|
+
return { added: false, wish_id: id, total: pool.length };
|
|
249
229
|
}
|
|
250
230
|
const id = makeWishId(name);
|
|
251
231
|
const doc = {
|
|
@@ -253,158 +233,80 @@ export class StateLedger {
|
|
|
253
233
|
name,
|
|
254
234
|
reason: entry.reason ?? '',
|
|
255
235
|
conditions,
|
|
256
|
-
...entry.muted !== undefined ? {
|
|
257
|
-
|
|
258
|
-
} : {},
|
|
259
|
-
added_at: ts
|
|
236
|
+
...(entry.muted !== undefined ? { muted: Boolean(entry.muted) } : {}),
|
|
237
|
+
added_at: ts,
|
|
260
238
|
};
|
|
261
|
-
const seq = this.insertEvent({
|
|
262
|
-
actor,
|
|
263
|
-
kind: 'wish.added',
|
|
264
|
-
subjectId: id,
|
|
265
|
-
payload: {
|
|
266
|
-
wish: doc
|
|
267
|
-
},
|
|
268
|
-
ts
|
|
269
|
-
});
|
|
239
|
+
const seq = this.insertEvent({ actor, kind: 'wish.added', subjectId: id, payload: { wish: doc }, ts });
|
|
270
240
|
this.insertItem('wish_pool', id, doc, seq ?? 0);
|
|
271
|
-
return {
|
|
272
|
-
added: true,
|
|
273
|
-
wish_id: id,
|
|
274
|
-
total: pool.length + 1
|
|
275
|
-
};
|
|
241
|
+
return { added: true, wish_id: id, total: pool.length + 1 };
|
|
276
242
|
});
|
|
277
243
|
return run();
|
|
278
244
|
}
|
|
245
|
+
/** 效用事件(幂等键 = 语义 event_id;verified 无归因自动降级 applied——纯函数同款纪律) */
|
|
279
246
|
appendUtilityEvent(ev, actor = 'tool:gotry_wish_pool_list') {
|
|
280
247
|
const full = gateUtilityEvent([], ev).events[0];
|
|
281
|
-
if (!full)
|
|
282
|
-
appended: false,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const run = this.db.transaction(()=>{
|
|
286
|
-
return this.insertEvent({
|
|
287
|
-
actor,
|
|
288
|
-
kind: 'memory_utility.event',
|
|
289
|
-
subjectId: full.wish_id,
|
|
290
|
-
payload: {
|
|
291
|
-
event: full
|
|
292
|
-
},
|
|
293
|
-
idemKey: `mu:${full.event_id}`
|
|
294
|
-
}) !== null;
|
|
248
|
+
if (!full)
|
|
249
|
+
return { appended: false, events: this.readUtilityEvents() };
|
|
250
|
+
const run = this.db.transaction(() => {
|
|
251
|
+
return this.insertEvent({ actor, kind: 'memory_utility.event', subjectId: full.wish_id, payload: { event: full }, idemKey: `mu:${full.event_id}` }) !== null;
|
|
295
252
|
});
|
|
296
|
-
return {
|
|
297
|
-
appended: run(),
|
|
298
|
-
events: this.readUtilityEvents()
|
|
299
|
-
};
|
|
253
|
+
return { appended: run(), events: this.readUtilityEvents() };
|
|
300
254
|
}
|
|
255
|
+
/** 时间线行程(守门 = appendTrip:必填/绝对日期/重叠冲突即停;幂等键 = trip_id) */
|
|
301
256
|
appendTripEvent(ev, actor = 'tool:gotry_trip_log') {
|
|
302
|
-
const run = this.db.transaction(()=>{
|
|
257
|
+
const run = this.db.transaction(() => {
|
|
303
258
|
const existing = this.readTrips();
|
|
304
259
|
const gate = gateTrip(existing, ev);
|
|
305
|
-
if (!gate.appended)
|
|
306
|
-
appended: false,
|
|
307
|
-
tripId: gate.tripId,
|
|
308
|
-
reason: gate.reason,
|
|
309
|
-
total: existing.length
|
|
310
|
-
};
|
|
260
|
+
if (!gate.appended)
|
|
261
|
+
return { appended: false, tripId: gate.tripId, reason: gate.reason, total: existing.length };
|
|
311
262
|
const full = gate.events[gate.events.length - 1];
|
|
312
|
-
this.insertEvent({
|
|
313
|
-
|
|
314
|
-
kind: 'trip.logged',
|
|
315
|
-
subjectId: full.trip_id,
|
|
316
|
-
payload: {
|
|
317
|
-
trip: full
|
|
318
|
-
},
|
|
319
|
-
idemKey: `trip:${full.trip_id}`
|
|
320
|
-
});
|
|
321
|
-
return {
|
|
322
|
-
appended: true,
|
|
323
|
-
tripId: full.trip_id,
|
|
324
|
-
total: gate.events.length
|
|
325
|
-
};
|
|
263
|
+
this.insertEvent({ actor, kind: 'trip.logged', subjectId: full.trip_id, payload: { trip: full }, idemKey: `trip:${full.trip_id}` });
|
|
264
|
+
return { appended: true, tripId: full.trip_id, total: gate.events.length };
|
|
326
265
|
});
|
|
327
266
|
return run();
|
|
328
267
|
}
|
|
268
|
+
/** 同行人档案(守门 = upsertCompanion:负面清单拒收/追加不删史/幂等) */
|
|
329
269
|
appendCompanion(patch, actor = 'tool:gotry_companion_save') {
|
|
330
|
-
const run = this.db.transaction(()=>{
|
|
270
|
+
const run = this.db.transaction(() => {
|
|
331
271
|
const profiles = this.readCompanions();
|
|
332
272
|
const res = upsertCompanion(profiles, patch);
|
|
333
|
-
if (!res.appended)
|
|
334
|
-
appended: false,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
};
|
|
339
|
-
this.insertEvent({
|
|
340
|
-
actor,
|
|
341
|
-
kind: 'companion.saved',
|
|
342
|
-
subjectId: res.companionId,
|
|
343
|
-
payload: {
|
|
344
|
-
patch
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
this.replaceItems('companions', res.profiles.map((p)=>[
|
|
348
|
-
p.companion_id,
|
|
349
|
-
p
|
|
350
|
-
]));
|
|
351
|
-
return {
|
|
352
|
-
appended: true,
|
|
353
|
-
companionId: res.companionId,
|
|
354
|
-
total: res.profiles.length
|
|
355
|
-
};
|
|
273
|
+
if (!res.appended)
|
|
274
|
+
return { appended: false, companionId: res.companionId, reason: res.reason, total: profiles.length };
|
|
275
|
+
this.insertEvent({ actor, kind: 'companion.saved', subjectId: res.companionId, payload: { patch } });
|
|
276
|
+
this.replaceItems('companions', res.profiles.map(p => [p.companion_id, p]));
|
|
277
|
+
return { appended: true, companionId: res.companionId, total: res.profiles.length };
|
|
356
278
|
});
|
|
357
279
|
return run();
|
|
358
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* 成行确认(原跨两文件写 → 单事务):verified_outcome 效用事件 + 可选时间线行程,
|
|
283
|
+
* 同事务提交,崩溃后要么全有要么全无(RFC §1.1 partial-writes 缺口的修复)。
|
|
284
|
+
*/
|
|
359
285
|
confirmOutcome(input, actor = 'tool:gotry_wish_pool_list') {
|
|
360
|
-
const run = this.db.transaction(()=>{
|
|
286
|
+
const run = this.db.transaction(() => {
|
|
361
287
|
const now = new Date().toISOString();
|
|
362
288
|
const full = gateUtilityEvent([], {
|
|
363
|
-
wish_id: input.wishId,
|
|
364
|
-
|
|
365
|
-
ts: now,
|
|
366
|
-
ctx: 'gotry_wish_pool_list.confirm',
|
|
367
|
-
detail: input.detail,
|
|
368
|
-
attribution: input.attribution
|
|
289
|
+
wish_id: input.wishId, kind: 'verified_outcome', ts: now,
|
|
290
|
+
ctx: 'gotry_wish_pool_list.confirm', detail: input.detail, attribution: input.attribution,
|
|
369
291
|
}).events[0];
|
|
370
|
-
const recorded = full
|
|
371
|
-
actor,
|
|
372
|
-
|
|
373
|
-
subjectId: full.wish_id,
|
|
374
|
-
payload: {
|
|
375
|
-
event: full
|
|
376
|
-
},
|
|
377
|
-
idemKey: `mu:${full.event_id}`
|
|
378
|
-
}) !== null : false;
|
|
292
|
+
const recorded = full
|
|
293
|
+
? this.insertEvent({ actor, kind: 'memory_utility.event', subjectId: full.wish_id, payload: { event: full }, idemKey: `mu:${full.event_id}` }) !== null
|
|
294
|
+
: false;
|
|
379
295
|
let trip;
|
|
380
296
|
if (input.trip) {
|
|
381
297
|
const existing = this.readTrips();
|
|
382
298
|
const gate = gateTrip(existing, input.trip);
|
|
383
299
|
if (gate.appended) {
|
|
384
300
|
const fullTrip = gate.events[gate.events.length - 1];
|
|
385
|
-
this.insertEvent({
|
|
386
|
-
actor,
|
|
387
|
-
kind: 'trip.logged',
|
|
388
|
-
subjectId: fullTrip.trip_id,
|
|
389
|
-
payload: {
|
|
390
|
-
trip: fullTrip
|
|
391
|
-
},
|
|
392
|
-
idemKey: `trip:${fullTrip.trip_id}`
|
|
393
|
-
});
|
|
301
|
+
this.insertEvent({ actor, kind: 'trip.logged', subjectId: fullTrip.trip_id, payload: { trip: fullTrip }, idemKey: `trip:${fullTrip.trip_id}` });
|
|
394
302
|
}
|
|
395
|
-
trip = {
|
|
396
|
-
appended: gate.appended,
|
|
397
|
-
tripId: gate.tripId,
|
|
398
|
-
reason: gate.reason
|
|
399
|
-
};
|
|
303
|
+
trip = { appended: gate.appended, tripId: gate.tripId, reason: gate.reason };
|
|
400
304
|
}
|
|
401
|
-
return {
|
|
402
|
-
recorded,
|
|
403
|
-
trip
|
|
404
|
-
};
|
|
305
|
+
return { recorded, trip };
|
|
405
306
|
});
|
|
406
307
|
return run();
|
|
407
308
|
}
|
|
309
|
+
// ---- 投影维护 ---------------------------------------------------------------
|
|
408
310
|
upsertItem(subject, itemId, doc) {
|
|
409
311
|
const ord = this.db.prepare(`SELECT ord FROM projection_items WHERE subject = ? AND item_id = ? AND tenant_id = ?`).get(subject, itemId, this.tenant)?.ord ?? this.nextOrd(subject);
|
|
410
312
|
this.db.prepare(`INSERT INTO projection_items (tenant_id, subject, item_id, doc, ord) VALUES (?, ?, ?, ?, ?)
|
|
@@ -417,113 +319,89 @@ export class StateLedger {
|
|
|
417
319
|
replaceItems(subject, items) {
|
|
418
320
|
this.db.prepare('DELETE FROM projection_items WHERE subject = ? AND tenant_id = ?').run(subject, this.tenant);
|
|
419
321
|
const ins = this.db.prepare('INSERT INTO projection_items (tenant_id, subject, item_id, doc, ord) VALUES (?, ?, ?, ?, ?)');
|
|
420
|
-
items.forEach(([id, doc], i)=>ins.run(this.tenant, subject, id, JSON.stringify(doc), i));
|
|
322
|
+
items.forEach(([id, doc], i) => ins.run(this.tenant, subject, id, JSON.stringify(doc), i));
|
|
421
323
|
}
|
|
422
324
|
nextOrd(subject) {
|
|
423
325
|
return (this.db.prepare('SELECT COALESCE(MAX(ord), -1) AS m FROM projection_items WHERE subject = ? AND tenant_id = ?').get(subject, this.tenant).m ?? -1) + 1;
|
|
424
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* fold 重建:DROP 全部投影 → 重放 events(可截到 toSeq)→ 重建投影。
|
|
329
|
+
* 与写路径共用同一套纯函数 → 直读投影与 fold 结果一致(账本/投影永不分叉的可验证性)。
|
|
330
|
+
*/
|
|
425
331
|
rebuildProjections(toSeq) {
|
|
426
|
-
const run = this.db.transaction(()=>{
|
|
332
|
+
const run = this.db.transaction(() => {
|
|
427
333
|
this.db.prepare('DELETE FROM projection_docs WHERE tenant_id = ?').run(this.tenant);
|
|
428
334
|
this.db.prepare('DELETE FROM projection_items WHERE tenant_id = ?').run(this.tenant);
|
|
429
|
-
const rows = (toSeq === undefined
|
|
430
|
-
this.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
this.
|
|
434
|
-
]);
|
|
435
|
-
for (const row of rows)this.foldEvent(row);
|
|
335
|
+
const rows = (toSeq === undefined
|
|
336
|
+
? this.db.prepare('SELECT seq, ts, kind, subject_id, payload FROM events WHERE tenant_id = ? ORDER BY seq')
|
|
337
|
+
: this.db.prepare('SELECT seq, ts, kind, subject_id, payload FROM events WHERE seq <= ? AND tenant_id = ? ORDER BY seq')).all(...(toSeq === undefined ? [this.tenant] : [toSeq, this.tenant]));
|
|
338
|
+
for (const row of rows)
|
|
339
|
+
this.foldEvent(row);
|
|
436
340
|
const wishes = this.db.prepare(`SELECT COUNT(*) AS n FROM projection_items WHERE subject = 'wish_pool' AND tenant_id = ?`).get(this.tenant).n;
|
|
437
341
|
const companions = this.db.prepare(`SELECT COUNT(*) AS n FROM projection_items WHERE subject = 'companions' AND tenant_id = ?`).get(this.tenant).n;
|
|
438
|
-
return {
|
|
439
|
-
events: rows.length,
|
|
440
|
-
wishes,
|
|
441
|
-
companions
|
|
442
|
-
};
|
|
342
|
+
return { events: rows.length, wishes, companions };
|
|
443
343
|
});
|
|
444
344
|
return run();
|
|
445
345
|
}
|
|
446
346
|
foldEvent(row) {
|
|
447
347
|
const p = JSON.parse(row.payload);
|
|
448
|
-
switch(row.kind){
|
|
449
|
-
case 'motivation.imported':
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
|
|
348
|
+
switch (row.kind) {
|
|
349
|
+
case 'motivation.imported': {
|
|
350
|
+
const doc = p['profile'];
|
|
351
|
+
this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
|
|
453
352
|
ON CONFLICT(tenant_id, subject) DO UPDATE SET doc = excluded.doc`).run(this.tenant, JSON.stringify(doc));
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
case 'motivation.patch':
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
updated_at: row.ts
|
|
464
|
-
};
|
|
465
|
-
this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
case 'motivation.patch': {
|
|
356
|
+
const current = this.readMotivation();
|
|
357
|
+
// 重建路径:时间戳 = 事件行 row.ts(账本事实);不在 fold 里再读时钟
|
|
358
|
+
const merged = mergeProfile(current, p['patch'], row.ts);
|
|
359
|
+
if (merged) {
|
|
360
|
+
const doc = { ...merged, updated_at: row.ts };
|
|
361
|
+
this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
|
|
466
362
|
ON CONFLICT(tenant_id, subject) DO UPDATE SET doc = excluded.doc`).run(this.tenant, JSON.stringify(doc));
|
|
467
|
-
}
|
|
468
|
-
break;
|
|
469
363
|
}
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
470
366
|
case 'wish.imported':
|
|
471
|
-
case 'wish.added':
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
{
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
const doc = {
|
|
484
|
-
...JSON.parse(rows.doc),
|
|
485
|
-
...patch
|
|
486
|
-
};
|
|
487
|
-
this.upsertItem('wish_pool', id, doc);
|
|
488
|
-
}
|
|
489
|
-
break;
|
|
490
|
-
}
|
|
491
|
-
case 'companion.imported':
|
|
492
|
-
{
|
|
493
|
-
const profile = p['profile'];
|
|
494
|
-
this.insertItem('companions', profile.companion_id, profile, row.seq);
|
|
495
|
-
break;
|
|
496
|
-
}
|
|
497
|
-
case 'companion.saved':
|
|
498
|
-
{
|
|
499
|
-
const patch = p['patch'];
|
|
500
|
-
const res = upsertCompanion(this.readCompanions(), patch);
|
|
501
|
-
this.replaceItems('companions', res.profiles.map((c)=>[
|
|
502
|
-
c.companion_id,
|
|
503
|
-
c
|
|
504
|
-
]));
|
|
505
|
-
break;
|
|
367
|
+
case 'wish.added': {
|
|
368
|
+
const wish = (p['wish'] ?? p['profile']);
|
|
369
|
+
this.insertItem('wish_pool', String(wish.wish_id ?? row.subject_id), wish, row.seq);
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
case 'wish.updated': {
|
|
373
|
+
const id = String(p['wish_id']);
|
|
374
|
+
const patch = (p['patch'] ?? {});
|
|
375
|
+
const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' AND item_id = ? AND tenant_id = ?`).get(id, this.tenant);
|
|
376
|
+
if (rows) {
|
|
377
|
+
const doc = { ...JSON.parse(rows.doc), ...patch };
|
|
378
|
+
this.upsertItem('wish_pool', id, doc);
|
|
506
379
|
}
|
|
507
|
-
default:
|
|
508
380
|
break;
|
|
381
|
+
}
|
|
382
|
+
case 'companion.imported': {
|
|
383
|
+
const profile = p['profile'];
|
|
384
|
+
this.insertItem('companions', profile.companion_id, profile, row.seq);
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case 'companion.saved': {
|
|
388
|
+
const patch = p['patch'];
|
|
389
|
+
const res = upsertCompanion(this.readCompanions(), patch);
|
|
390
|
+
this.replaceItems('companions', res.profiles.map(c => [c.companion_id, c]));
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
default:
|
|
394
|
+
break; // 日志类事件(memory_utility/trip/async/write/forget)无投影
|
|
509
395
|
}
|
|
510
396
|
}
|
|
397
|
+
// ---- durable 工单(TS-3) -----------------------------------------------------
|
|
511
398
|
createWorkflowRun(input, actor = 'system:async-request') {
|
|
512
399
|
const ts = new Date().toISOString();
|
|
513
|
-
const run = this.db.transaction(()=>{
|
|
400
|
+
const run = this.db.transaction(() => {
|
|
514
401
|
this.db.prepare(`INSERT INTO workflow_runs (tenant_id, id, goal, status, ticket_json, state_json, deliverable, created, updated)
|
|
515
402
|
VALUES (?, ?, ?, 'pending', ?, ?, NULL, ?, ?)
|
|
516
403
|
ON CONFLICT(tenant_id, id) DO UPDATE SET updated = excluded.updated`).run(this.tenant, input.id, input.goal, JSON.stringify(input.ticket), JSON.stringify(input.state), ts, ts);
|
|
517
|
-
this.insertEvent({
|
|
518
|
-
actor,
|
|
519
|
-
kind: 'async.run_created',
|
|
520
|
-
subjectId: input.id,
|
|
521
|
-
payload: {
|
|
522
|
-
ticket: input.ticket
|
|
523
|
-
},
|
|
524
|
-
runId: input.id,
|
|
525
|
-
ts
|
|
526
|
-
});
|
|
404
|
+
this.insertEvent({ actor, kind: 'async.run_created', subjectId: input.id, payload: { ticket: input.ticket }, runId: input.id, ts });
|
|
527
405
|
});
|
|
528
406
|
run();
|
|
529
407
|
}
|
|
@@ -544,24 +422,22 @@ export class StateLedger {
|
|
|
544
422
|
this.db.prepare(`UPDATE workflow_steps SET status = 'done', result = ?, done_ts = ? WHERE run_id = ? AND name = ? AND tenant_id = ?`).run(JSON.stringify(result), new Date().toISOString(), runId, name, this.tenant);
|
|
545
423
|
}
|
|
546
424
|
finishWorkflowRun(id, status, eventKind, deliverable, terminalOutcome, actor = 'system:async-collect') {
|
|
547
|
-
const run = this.db.transaction(()=>{
|
|
425
|
+
const run = this.db.transaction(() => {
|
|
548
426
|
const update = this.db.prepare(`UPDATE workflow_runs SET status = ?, deliverable = ?, updated = ?
|
|
549
427
|
WHERE id = ? AND tenant_id = ? AND status = 'pending'`).run(status, deliverable, new Date().toISOString(), id, this.tenant);
|
|
550
428
|
if (update.changes === 0) {
|
|
551
429
|
const existing = this.getWorkflowRun(id);
|
|
552
|
-
if (existing?.status === status)
|
|
430
|
+
if (existing?.status === status)
|
|
431
|
+
return;
|
|
553
432
|
throw new Error(`workflow ${id} cannot transition ${existing?.status ?? 'missing'} -> ${status}`);
|
|
554
433
|
}
|
|
555
434
|
this.insertEvent({
|
|
556
435
|
actor,
|
|
557
436
|
kind: eventKind,
|
|
558
437
|
subjectId: id,
|
|
559
|
-
payload: {
|
|
560
|
-
bytes: deliverable.length,
|
|
561
|
-
terminal_outcome: terminalOutcome
|
|
562
|
-
},
|
|
438
|
+
payload: { bytes: deliverable.length, terminal_outcome: terminalOutcome },
|
|
563
439
|
idemKey: `async-terminal:${id}`,
|
|
564
|
-
runId: id
|
|
440
|
+
runId: id,
|
|
565
441
|
});
|
|
566
442
|
});
|
|
567
443
|
run();
|
|
@@ -576,149 +452,112 @@ export class StateLedger {
|
|
|
576
452
|
const row = this.db.prepare(`SELECT payload FROM events
|
|
577
453
|
WHERE tenant_id = ? AND run_id = ? AND kind IN ('async.settled', 'async.failed')
|
|
578
454
|
ORDER BY seq DESC LIMIT 1`).get(this.tenant, id);
|
|
579
|
-
if (!row)
|
|
455
|
+
if (!row)
|
|
456
|
+
return null;
|
|
580
457
|
try {
|
|
581
458
|
const payload = JSON.parse(row.payload);
|
|
582
459
|
return payload.terminal_outcome ?? null;
|
|
583
|
-
}
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
584
462
|
return null;
|
|
585
463
|
}
|
|
586
464
|
}
|
|
465
|
+
// ---- WriteGate saga 基座(TS-4, RFC S4 L2/L3 物理预备) -----------------------
|
|
466
|
+
/** 登记一笔待确认外部写(幂等键去重:同一确认不可能登记两次)。L2 = 只登记不执行。 */
|
|
587
467
|
requestPendingWrite(input, actor = 'system:writegate') {
|
|
588
468
|
const ts = new Date().toISOString();
|
|
589
|
-
const run = this.db.transaction(()=>{
|
|
469
|
+
const run = this.db.transaction(() => {
|
|
590
470
|
const info = this.db.prepare(`INSERT INTO pending_writes (tenant_id, idem_key, seam, payload, status, created, updated)
|
|
591
471
|
VALUES (?, ?, ?, ?, 'pending', ?, ?) ON CONFLICT(tenant_id, idem_key) DO NOTHING`).run(this.tenant, input.idemKey, input.seam, JSON.stringify(input.payload ?? {}), ts, ts);
|
|
592
472
|
if (info.changes === 0) {
|
|
593
473
|
const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ? AND tenant_id = ?').get(input.idemKey, this.tenant);
|
|
594
|
-
return {
|
|
595
|
-
created: false,
|
|
596
|
-
status: row.status
|
|
597
|
-
};
|
|
474
|
+
return { created: false, status: row.status };
|
|
598
475
|
}
|
|
599
|
-
this.insertEvent({
|
|
600
|
-
|
|
601
|
-
kind: 'write.pending',
|
|
602
|
-
subjectId: input.idemKey,
|
|
603
|
-
payload: {
|
|
604
|
-
seam: input.seam,
|
|
605
|
-
payload: input.payload
|
|
606
|
-
},
|
|
607
|
-
idemKey: `pw:${sha(input.idemKey)}:pending`
|
|
608
|
-
});
|
|
609
|
-
return {
|
|
610
|
-
created: true,
|
|
611
|
-
status: 'pending'
|
|
612
|
-
};
|
|
476
|
+
this.insertEvent({ actor, kind: 'write.pending', subjectId: input.idemKey, payload: { seam: input.seam, payload: input.payload }, idemKey: `pw:${sha(input.idemKey)}:pending` });
|
|
477
|
+
return { created: true, status: 'pending' };
|
|
613
478
|
});
|
|
614
479
|
return run();
|
|
615
480
|
}
|
|
481
|
+
/** L3:具名 seam 确认——pending → confirmed,携 receipt(外部世界回执) */
|
|
616
482
|
confirmPendingWrite(idemKey, receipt, actor = 'system:writegate') {
|
|
617
|
-
const run = this.db.transaction(()=>{
|
|
483
|
+
const run = this.db.transaction(() => {
|
|
618
484
|
const info = this.db.prepare(`UPDATE pending_writes SET status = 'confirmed', receipt = ?, updated = ? WHERE idem_key = ? AND status = 'pending' AND tenant_id = ?`).run(receipt, new Date().toISOString(), idemKey, this.tenant);
|
|
619
485
|
if (info.changes === 0) {
|
|
620
486
|
const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ? AND tenant_id = ?').get(idemKey, this.tenant);
|
|
621
|
-
return {
|
|
622
|
-
ok: false,
|
|
623
|
-
status: row?.status ?? 'missing'
|
|
624
|
-
};
|
|
487
|
+
return { ok: false, status: row?.status ?? 'missing' };
|
|
625
488
|
}
|
|
626
|
-
this.insertEvent({
|
|
627
|
-
|
|
628
|
-
kind: 'write.confirmed',
|
|
629
|
-
subjectId: idemKey,
|
|
630
|
-
payload: {
|
|
631
|
-
receipt
|
|
632
|
-
}
|
|
633
|
-
});
|
|
634
|
-
return {
|
|
635
|
-
ok: true,
|
|
636
|
-
status: 'confirmed'
|
|
637
|
-
};
|
|
489
|
+
this.insertEvent({ actor, kind: 'write.confirmed', subjectId: idemKey, payload: { receipt } });
|
|
490
|
+
return { ok: true, status: 'confirmed' };
|
|
638
491
|
});
|
|
639
492
|
return run();
|
|
640
493
|
}
|
|
494
|
+
/** saga 补偿:pending/confirmed → compensated(SagaLLM 式补偿语义) */
|
|
641
495
|
compensatePendingWrite(idemKey, note, actor = 'system:writegate') {
|
|
642
|
-
const run = this.db.transaction(()=>{
|
|
496
|
+
const run = this.db.transaction(() => {
|
|
643
497
|
const info = this.db.prepare(`UPDATE pending_writes SET status = 'compensated', receipt = COALESCE(receipt, ?), updated = ? WHERE idem_key = ? AND status != 'compensated' AND tenant_id = ?`).run(note, new Date().toISOString(), idemKey, this.tenant);
|
|
644
498
|
if (info.changes === 0) {
|
|
645
499
|
const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ? AND tenant_id = ?').get(idemKey, this.tenant);
|
|
646
|
-
return {
|
|
647
|
-
ok: false,
|
|
648
|
-
status: row?.status ?? 'missing'
|
|
649
|
-
};
|
|
500
|
+
return { ok: false, status: row?.status ?? 'missing' };
|
|
650
501
|
}
|
|
651
|
-
this.insertEvent({
|
|
652
|
-
|
|
653
|
-
kind: 'write.compensated',
|
|
654
|
-
subjectId: idemKey,
|
|
655
|
-
payload: {
|
|
656
|
-
note
|
|
657
|
-
}
|
|
658
|
-
});
|
|
659
|
-
return {
|
|
660
|
-
ok: true,
|
|
661
|
-
status: 'compensated'
|
|
662
|
-
};
|
|
502
|
+
this.insertEvent({ actor, kind: 'write.compensated', subjectId: idemKey, payload: { note } });
|
|
503
|
+
return { ok: true, status: 'compensated' };
|
|
663
504
|
});
|
|
664
505
|
return run();
|
|
665
506
|
}
|
|
666
507
|
listPendingWrites() {
|
|
667
508
|
return this.db.prepare('SELECT idem_key, seam, status, receipt, created FROM pending_writes WHERE tenant_id = ? ORDER BY created').all(this.tenant);
|
|
668
509
|
}
|
|
510
|
+
/** what-if 分叉:VACUUM INTO 副本,预演不触正本(LoopX「先只读投影后执行」的物理化) */
|
|
669
511
|
forkWhatIf(destPath) {
|
|
670
512
|
this.db.prepare('VACUUM INTO ?').run(destPath);
|
|
671
513
|
return destPath;
|
|
672
514
|
}
|
|
515
|
+
// ---- forget(D5:物理硬删,审计只留一行) ---------------------------------------
|
|
516
|
+
/** 物理删除某主体的全部事件并重建投影;删除本身留一行审计事件(红线 6「可删除」)。 */
|
|
673
517
|
forgetSubject(subjects, actor = 'system:state-cli') {
|
|
674
|
-
const run = this.db.transaction(()=>{
|
|
518
|
+
const run = this.db.transaction(() => {
|
|
675
519
|
let deleted = 0;
|
|
676
|
-
for (const s of subjects){
|
|
677
|
-
const placeholders = s.kinds.map(()=>'?').join(',');
|
|
678
|
-
const info = this.db.prepare(
|
|
520
|
+
for (const s of subjects) {
|
|
521
|
+
const placeholders = s.kinds.map(() => '?').join(',');
|
|
522
|
+
const info = this.db.prepare('DELETE FROM events WHERE subject_id = ? AND tenant_id = ? AND kind IN (' + placeholders + ')').run(s.subjectId, this.tenant, ...s.kinds);
|
|
679
523
|
deleted += info.changes;
|
|
680
524
|
}
|
|
681
|
-
this.insertEvent({
|
|
682
|
-
actor,
|
|
683
|
-
kind: 'forget.executed',
|
|
684
|
-
payload: {
|
|
685
|
-
subjects
|
|
686
|
-
}
|
|
687
|
-
});
|
|
525
|
+
this.insertEvent({ actor, kind: 'forget.executed', payload: { subjects } });
|
|
688
526
|
this.rebuildProjections();
|
|
689
|
-
return {
|
|
690
|
-
deleted
|
|
691
|
-
};
|
|
527
|
+
return { deleted };
|
|
692
528
|
});
|
|
693
529
|
return run();
|
|
694
530
|
}
|
|
695
531
|
close() {
|
|
696
532
|
this.db.close();
|
|
697
|
-
for (const [key, value] of openLedgers)
|
|
533
|
+
for (const [key, value] of openLedgers)
|
|
534
|
+
if (value === this)
|
|
535
|
+
openLedgers.delete(key);
|
|
698
536
|
}
|
|
699
537
|
}
|
|
538
|
+
// ---- 打开/迁移(one-shot D2;模块级缓存避免每次读开新句柄) ----------------------
|
|
700
539
|
const openLedgers = new Map();
|
|
701
540
|
export function openDb(stateRoot, tenant = 'local') {
|
|
702
541
|
const path = ledgerDbPath(stateRoot);
|
|
703
542
|
const cacheKey = `${path}::${tenant}`;
|
|
704
543
|
const cached = openLedgers.get(cacheKey);
|
|
705
|
-
if (cached)
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
});
|
|
544
|
+
if (cached)
|
|
545
|
+
return cached;
|
|
546
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
709
547
|
const db = new Database(path);
|
|
710
548
|
db.pragma('journal_mode = WAL');
|
|
711
549
|
db.pragma('synchronous = NORMAL');
|
|
712
550
|
db.pragma('busy_timeout = 5000');
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
});
|
|
551
|
+
// schema v1→v2 迁移:v1 无 tenant_id 列(user_version 未设置过,实际为 0)
|
|
552
|
+
const version = db.pragma('user_version', { simple: true });
|
|
716
553
|
if (version < 2) {
|
|
554
|
+
// 检测是否真有 v1 数据(有 events 表但无 tenant_id 列)
|
|
717
555
|
const hasEventsTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='events'").get() !== undefined;
|
|
718
|
-
const eventsHasTenant = hasEventsTable && db.prepare("PRAGMA table_info(events)").all().some(
|
|
556
|
+
const eventsHasTenant = hasEventsTable && db.prepare("PRAGMA table_info(events)").all().some(c => c.name === 'tenant_id');
|
|
719
557
|
const isV1 = hasEventsTable && !eventsHasTenant;
|
|
720
|
-
db.transaction(()=>{
|
|
558
|
+
db.transaction(() => {
|
|
721
559
|
if (isV1) {
|
|
560
|
+
// v1 → v2:events 加 tenant_id,回填 'local';投影/工单/pending_writes 强制重建(旧表无 tenant_id)
|
|
722
561
|
db.exec(`
|
|
723
562
|
ALTER TABLE events ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'local';
|
|
724
563
|
DROP INDEX IF EXISTS events_idem;
|
|
@@ -729,8 +568,10 @@ export function openDb(stateRoot, tenant = 'local') {
|
|
|
729
568
|
DROP TABLE IF EXISTS workflow_steps;
|
|
730
569
|
DROP TABLE IF EXISTS pending_writes;
|
|
731
570
|
`);
|
|
732
|
-
}
|
|
733
|
-
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
// v2 幂等:检查 workflow_runs 是否缺 tenant_id(修一次跑坏的迁移)
|
|
574
|
+
const wrHasTenant = db.prepare("PRAGMA table_info(workflow_runs)").all().some(c => c.name === 'tenant_id');
|
|
734
575
|
if (!wrHasTenant && db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='workflow_runs'").get()) {
|
|
735
576
|
db.exec(`
|
|
736
577
|
DROP TABLE IF EXISTS workflow_runs;
|
|
@@ -743,139 +584,122 @@ export function openDb(stateRoot, tenant = 'local') {
|
|
|
743
584
|
}
|
|
744
585
|
db.exec(SCHEMA);
|
|
745
586
|
db.pragma('user_version = 2');
|
|
746
|
-
db.prepare('INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v').run('schema_version',
|
|
587
|
+
db.prepare('INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v').run('schema_version', SCHEMA_VERSION);
|
|
747
588
|
})();
|
|
589
|
+
// v1 升级后:旧单用户事件只可安全归入 local;即使调用者打开非 local 租户,也只重建 local 投影。
|
|
748
590
|
if (isV1) {
|
|
749
|
-
const ledger = new StateLedger(db, stateRoot,
|
|
591
|
+
const ledger = new StateLedger(db, stateRoot, 'local');
|
|
750
592
|
ledger.rebuildProjections();
|
|
751
593
|
}
|
|
752
|
-
}
|
|
753
|
-
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
db.exec(SCHEMA); // 幂等建表(IF NOT EXISTS)
|
|
754
597
|
db.prepare('INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v').run('schema_version', '2');
|
|
755
598
|
}
|
|
756
599
|
const ledger = new StateLedger(db, stateRoot, tenant);
|
|
757
600
|
openLedgers.set(cacheKey, ledger);
|
|
758
601
|
return ledger;
|
|
759
602
|
}
|
|
603
|
+
/** 只读场景:账本不存在返回 null(调用方走文件回退) */
|
|
760
604
|
export function openLedgerIfExists(stateRoot, tenant = 'local') {
|
|
761
|
-
if (!ledgerExists(stateRoot))
|
|
605
|
+
if (!ledgerExists(stateRoot))
|
|
606
|
+
return null;
|
|
762
607
|
return openDb(stateRoot, tenant);
|
|
763
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* 确保账本可用(写路径必经):
|
|
611
|
+
* - DB 已存在 → 直接用
|
|
612
|
+
* - 无 DB 无旧文件 → 建空账本(fresh root,smoke/测试形态)
|
|
613
|
+
* - 无 DB 有旧文件 → 快照 → 建账本 → 旧数据单事务导入为 events(one-shot;
|
|
614
|
+
* 导入幂等键 + kv 旗标双保险,并发 ensure 不重复导入)
|
|
615
|
+
*/
|
|
764
616
|
export function ensureLedger(stateRoot, tenant = 'local') {
|
|
765
|
-
if (ledgerExists(stateRoot))
|
|
617
|
+
if (ledgerExists(stateRoot))
|
|
618
|
+
return openDb(stateRoot, tenant);
|
|
766
619
|
const dir = stateDirOf(stateRoot);
|
|
767
620
|
const hasLegacy = legacyFilesPresent(stateRoot);
|
|
768
621
|
let backupDir = null;
|
|
769
622
|
if (hasLegacy) {
|
|
770
623
|
backupDir = join(dir, 'pre-ledger-backup');
|
|
771
|
-
mkdirSync(backupDir, {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
if (existsSync(join(dir, f))) copyFileSync(join(dir, f), join(backupDir, f));
|
|
624
|
+
mkdirSync(backupDir, { recursive: true });
|
|
625
|
+
for (const f of LEGACY_FILES) {
|
|
626
|
+
if (existsSync(join(dir, f)))
|
|
627
|
+
copyFileSync(join(dir, f), join(backupDir, f));
|
|
776
628
|
}
|
|
777
629
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
630
|
+
if (hasLegacy) {
|
|
631
|
+
// 旧 JSON/JSONL 是单用户本地状态,没有可审计租户归属;迁移边界=只归入 local,绝不按调用 tenant 猜测或搬移。
|
|
632
|
+
const localLedger = openDb(stateRoot, 'local');
|
|
633
|
+
importLegacyInto(localLedger, dir);
|
|
634
|
+
return tenant === 'local' ? localLedger : openDb(stateRoot, tenant);
|
|
635
|
+
}
|
|
636
|
+
return openDb(stateRoot, tenant);
|
|
781
637
|
}
|
|
782
638
|
function importLegacyInto(ledger, dir) {
|
|
783
|
-
const imported = {
|
|
784
|
-
|
|
785
|
-
wishes: 0,
|
|
786
|
-
utilityEvents: 0,
|
|
787
|
-
trips: 0,
|
|
788
|
-
companions: 0
|
|
789
|
-
};
|
|
790
|
-
const run = ledger.db.transaction(()=>{
|
|
639
|
+
const imported = { motivation: false, wishes: 0, utilityEvents: 0, trips: 0, companions: 0 };
|
|
640
|
+
const run = ledger.db.transaction(() => {
|
|
791
641
|
const flag = ledger.db.prepare(`SELECT v FROM kv WHERE k = 'legacy_imported_v1'`).get();
|
|
792
|
-
if (flag)
|
|
793
|
-
|
|
642
|
+
if (flag)
|
|
643
|
+
return;
|
|
644
|
+
const readJsonSafe = (p) => {
|
|
794
645
|
try {
|
|
795
646
|
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
796
|
-
}
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
797
649
|
return null;
|
|
798
650
|
}
|
|
799
651
|
};
|
|
652
|
+
// 动机画像:整档快照事件(fold 直接落投影)
|
|
800
653
|
const profile = readJsonSafe(join(dir, 'motivation-profile.json'));
|
|
801
654
|
if (profile) {
|
|
802
|
-
if (ledger.insertEvent({
|
|
803
|
-
|
|
804
|
-
kind: 'motivation.imported',
|
|
805
|
-
subjectId: 'motivation',
|
|
806
|
-
payload: {
|
|
807
|
-
profile
|
|
808
|
-
},
|
|
809
|
-
idemKey: 'import:motivation'
|
|
810
|
-
}) !== null) imported.motivation = true;
|
|
655
|
+
if (ledger.insertEvent({ actor: 'system:migrate', kind: 'motivation.imported', subjectId: 'motivation', payload: { profile }, idemKey: 'import:motivation' }) !== null)
|
|
656
|
+
imported.motivation = true;
|
|
811
657
|
}
|
|
658
|
+
// 愿望池:逐条导入(保留原 wish_id)
|
|
812
659
|
const pool = readJsonSafe(join(dir, 'wish-pool.json'));
|
|
813
660
|
if (Array.isArray(pool)) {
|
|
814
|
-
for (const w of pool){
|
|
661
|
+
for (const w of pool) {
|
|
815
662
|
const id = String(w['wish_id'] ?? makeWishId(String(w['name'] ?? '')));
|
|
816
|
-
const wish = {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
};
|
|
820
|
-
if (ledger.insertEvent({
|
|
821
|
-
actor: 'system:migrate',
|
|
822
|
-
kind: 'wish.imported',
|
|
823
|
-
subjectId: id,
|
|
824
|
-
payload: {
|
|
825
|
-
wish
|
|
826
|
-
},
|
|
827
|
-
idemKey: `import:wish:${id}`
|
|
828
|
-
}) !== null) imported.wishes++;
|
|
663
|
+
const wish = { ...w, wish_id: id };
|
|
664
|
+
if (ledger.insertEvent({ actor: 'system:migrate', kind: 'wish.imported', subjectId: id, payload: { wish }, idemKey: `import:wish:${id}` }) !== null)
|
|
665
|
+
imported.wishes++;
|
|
829
666
|
}
|
|
830
667
|
}
|
|
668
|
+
// 效用流:逐行导入(语义 event_id 即幂等键)
|
|
831
669
|
try {
|
|
832
670
|
const lines = readFileSync(join(dir, 'memory-utility.jsonl'), 'utf-8').split('\n').filter(Boolean);
|
|
833
|
-
for (const line of lines){
|
|
671
|
+
for (const line of lines) {
|
|
834
672
|
try {
|
|
835
673
|
const ev = JSON.parse(line);
|
|
836
|
-
if (ledger.insertEvent({
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
payload: {
|
|
841
|
-
event: ev
|
|
842
|
-
},
|
|
843
|
-
idemKey: `mu:${ev.event_id}`
|
|
844
|
-
}) !== null) imported.utilityEvents++;
|
|
845
|
-
} catch {}
|
|
674
|
+
if (ledger.insertEvent({ actor: 'system:migrate', kind: 'memory_utility.event', subjectId: ev.wish_id, payload: { event: ev }, idemKey: `mu:${ev.event_id}` }) !== null)
|
|
675
|
+
imported.utilityEvents++;
|
|
676
|
+
}
|
|
677
|
+
catch { /* 脏行跳过:导入永不因单行失败而中断 */ }
|
|
846
678
|
}
|
|
847
|
-
}
|
|
679
|
+
}
|
|
680
|
+
catch { /* 文件不存在 */ }
|
|
681
|
+
// 时间线:逐行导入(trip_id 幂等)
|
|
848
682
|
try {
|
|
849
683
|
const lines = readFileSync(join(dir, 'trips.jsonl'), 'utf-8').split('\n').filter(Boolean);
|
|
850
|
-
for (const line of lines){
|
|
684
|
+
for (const line of lines) {
|
|
851
685
|
try {
|
|
852
686
|
const trip = JSON.parse(line);
|
|
853
|
-
if (ledger.insertEvent({
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
payload: {
|
|
858
|
-
trip
|
|
859
|
-
},
|
|
860
|
-
idemKey: `trip:${trip.trip_id}`
|
|
861
|
-
}) !== null) imported.trips++;
|
|
862
|
-
} catch {}
|
|
687
|
+
if (ledger.insertEvent({ actor: 'system:migrate', kind: 'trip.logged', subjectId: trip.trip_id, payload: { trip }, idemKey: `trip:${trip.trip_id}` }) !== null)
|
|
688
|
+
imported.trips++;
|
|
689
|
+
}
|
|
690
|
+
catch { /* 脏行跳过 */ }
|
|
863
691
|
}
|
|
864
|
-
}
|
|
692
|
+
}
|
|
693
|
+
catch { /* 文件不存在 */ }
|
|
694
|
+
// 同行人:逐条导入(保留 companion_id)
|
|
865
695
|
const companions = readJsonSafe(join(dir, 'companions.json'));
|
|
866
696
|
if (Array.isArray(companions)) {
|
|
867
|
-
for (const c of companions){
|
|
697
|
+
for (const c of companions) {
|
|
868
698
|
const id = String(c['companion_id'] ?? '');
|
|
869
|
-
if (!id)
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
subjectId: id,
|
|
874
|
-
payload: {
|
|
875
|
-
profile: c
|
|
876
|
-
},
|
|
877
|
-
idemKey: `import:companion:${id}`
|
|
878
|
-
}) !== null) imported.companions++;
|
|
699
|
+
if (!id)
|
|
700
|
+
continue;
|
|
701
|
+
if (ledger.insertEvent({ actor: 'system:migrate', kind: 'companion.imported', subjectId: id, payload: { profile: c }, idemKey: `import:companion:${id}` }) !== null)
|
|
702
|
+
imported.companions++;
|
|
879
703
|
}
|
|
880
704
|
}
|
|
881
705
|
ledger.rebuildProjections();
|
|
@@ -884,45 +708,60 @@ function importLegacyInto(ledger, dir) {
|
|
|
884
708
|
run();
|
|
885
709
|
return imported;
|
|
886
710
|
}
|
|
711
|
+
// ---- 读路径统一出口:账本优先,未迁移 root 回退旧文件(只读,绝不反向写) ---------
|
|
887
712
|
function readLegacyJson(stateRoot, name, fallback) {
|
|
888
713
|
try {
|
|
889
714
|
return JSON.parse(readFileSync(join(stateDirOf(stateRoot), name), 'utf-8'));
|
|
890
|
-
}
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
891
717
|
return fallback;
|
|
892
718
|
}
|
|
893
719
|
}
|
|
894
720
|
function readLegacyJsonl(stateRoot, name) {
|
|
895
721
|
try {
|
|
896
|
-
return readFileSync(join(stateDirOf(stateRoot), name), 'utf-8').split('\n').filter(Boolean).map(
|
|
897
|
-
}
|
|
722
|
+
return readFileSync(join(stateDirOf(stateRoot), name), 'utf-8').split('\n').filter(Boolean).map(l => JSON.parse(l));
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
898
725
|
return [];
|
|
899
726
|
}
|
|
900
727
|
}
|
|
901
|
-
export function readMotivationWithFallback(stateRoot) {
|
|
902
|
-
const ledger = openLedgerIfExists(stateRoot);
|
|
903
|
-
if (ledger)
|
|
728
|
+
export function readMotivationWithFallback(stateRoot, tenant = 'local') {
|
|
729
|
+
const ledger = openLedgerIfExists(stateRoot, tenant);
|
|
730
|
+
if (ledger)
|
|
731
|
+
return ledger.readMotivation();
|
|
732
|
+
if (tenant !== 'local')
|
|
733
|
+
return null;
|
|
904
734
|
return readLegacyJson(stateRoot, 'motivation-profile.json', null);
|
|
905
735
|
}
|
|
906
|
-
export function readWishPoolWithFallback(stateRoot) {
|
|
907
|
-
const ledger = openLedgerIfExists(stateRoot);
|
|
908
|
-
if (ledger)
|
|
736
|
+
export function readWishPoolWithFallback(stateRoot, tenant = 'local') {
|
|
737
|
+
const ledger = openLedgerIfExists(stateRoot, tenant);
|
|
738
|
+
if (ledger)
|
|
739
|
+
return ledger.readWishPool();
|
|
740
|
+
if (tenant !== 'local')
|
|
741
|
+
return [];
|
|
909
742
|
return readLegacyJson(stateRoot, 'wish-pool.json', []);
|
|
910
743
|
}
|
|
911
|
-
export function readUtilityEventsWithFallback(stateRoot) {
|
|
912
|
-
const ledger = openLedgerIfExists(stateRoot);
|
|
913
|
-
if (ledger)
|
|
744
|
+
export function readUtilityEventsWithFallback(stateRoot, tenant = 'local') {
|
|
745
|
+
const ledger = openLedgerIfExists(stateRoot, tenant);
|
|
746
|
+
if (ledger)
|
|
747
|
+
return ledger.readUtilityEvents();
|
|
748
|
+
if (tenant !== 'local')
|
|
749
|
+
return [];
|
|
914
750
|
return readLegacyJsonl(stateRoot, 'memory-utility.jsonl');
|
|
915
751
|
}
|
|
916
|
-
export function readTripsWithFallback(stateRoot) {
|
|
917
|
-
const ledger = openLedgerIfExists(stateRoot);
|
|
918
|
-
if (ledger)
|
|
752
|
+
export function readTripsWithFallback(stateRoot, tenant = 'local') {
|
|
753
|
+
const ledger = openLedgerIfExists(stateRoot, tenant);
|
|
754
|
+
if (ledger)
|
|
755
|
+
return ledger.readTrips();
|
|
756
|
+
if (tenant !== 'local')
|
|
757
|
+
return [];
|
|
919
758
|
return readLegacyJsonl(stateRoot, 'trips.jsonl');
|
|
920
759
|
}
|
|
921
|
-
export function readCompanionsWithFallback(stateRoot) {
|
|
922
|
-
const ledger = openLedgerIfExists(stateRoot);
|
|
923
|
-
if (ledger)
|
|
760
|
+
export function readCompanionsWithFallback(stateRoot, tenant = 'local') {
|
|
761
|
+
const ledger = openLedgerIfExists(stateRoot, tenant);
|
|
762
|
+
if (ledger)
|
|
763
|
+
return ledger.readCompanions();
|
|
764
|
+
if (tenant !== 'local')
|
|
765
|
+
return [];
|
|
924
766
|
return readLegacyJson(stateRoot, 'companions.json', []);
|
|
925
767
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
//# sourceURL=ts/src/state-ledger.ts
|