@danceiny/gotry 0.0.1-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/bin/gotry-inner.js +214 -0
- package/bin/gotry-stdio-ask.js +59 -0
- package/bin/gotry.js +28 -0
- package/cordis.gotry-patch.yml +86 -0
- package/data/flights_2026.json +218 -0
- package/data/golden_erhai.json +92 -0
- package/data/golden_trip_2026.json +75 -0
- package/data/hotels_2026.json +151 -0
- package/data/openflights-skeleton.json +895 -0
- package/data/time-slot-eval.json +384 -0
- package/data/yunnan-pack.json +202 -0
- package/dist/capabilities/agent-reach-bridge.py +90 -0
- package/dist/capabilities/agent-reach-deep.js +172 -0
- package/dist/capabilities/agent-reach.js +204 -0
- package/dist/capabilities/anything.js +139 -0
- package/dist/capabilities/flyai.js +131 -0
- package/dist/capabilities/hbcli.js +143 -0
- package/dist/capabilities/incident-log.js +100 -0
- package/dist/capabilities/opensky.js +74 -0
- package/dist/capabilities/session/action-cache.js +120 -0
- package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
- package/dist/capabilities/session/adapters/meituan-local.js +73 -0
- package/dist/capabilities/session/extract.js +40 -0
- package/dist/capabilities/session/read-guard.js +99 -0
- package/dist/capabilities/session/transport.js +101 -0
- package/dist/capabilities/session-search.js +93 -0
- package/dist/capabilities/weather.js +180 -0
- package/dist/scripts/action-cache-tests.js +107 -0
- package/dist/scripts/agent-reach-deep-tests.js +54 -0
- package/dist/scripts/agent-reach-tests.js +38 -0
- package/dist/scripts/agent-reach-wrapper-tests.js +79 -0
- package/dist/scripts/anything-tests.js +95 -0
- package/dist/scripts/async-collect.js +37 -0
- package/dist/scripts/companion-tests.js +112 -0
- package/dist/scripts/diff-test.js +39 -0
- package/dist/scripts/engine-run.js +14 -0
- package/dist/scripts/engine-tests.js +45 -0
- package/dist/scripts/hbcli-tests.js +101 -0
- package/dist/scripts/incident-tests.js +96 -0
- package/dist/scripts/journey-tests.js +45 -0
- package/dist/scripts/ledger-tests.js +365 -0
- package/dist/scripts/ledger-workflow-crash.js +42 -0
- package/dist/scripts/memory-capture-tests.js +77 -0
- package/dist/scripts/memory-decay-tests.js +83 -0
- package/dist/scripts/memory-metrics.js +24 -0
- package/dist/scripts/nudge-digest.js +83 -0
- package/dist/scripts/opensky-check.js +20 -0
- package/dist/scripts/opensky-tests.js +52 -0
- package/dist/scripts/probe-poi-tests.js +88 -0
- package/dist/scripts/publish-preverify.js +68 -0
- package/dist/scripts/replay-async.js +44 -0
- package/dist/scripts/replay-real.js +38 -0
- package/dist/scripts/replay.js +116 -0
- package/dist/scripts/session-attach-diagnose.js +27 -0
- package/dist/scripts/session-attach-poc.js +65 -0
- package/dist/scripts/session-extract-tests.js +81 -0
- package/dist/scripts/session-tests.js +162 -0
- package/dist/scripts/skeleton-check.js +37 -0
- package/dist/scripts/skeleton-integration-test.js +22 -0
- package/dist/scripts/skills-contract-tests.js +85 -0
- package/dist/scripts/smoke.js +328 -0
- package/dist/scripts/state-cli-tests.js +136 -0
- package/dist/scripts/state-cli.js +248 -0
- package/dist/scripts/time-eval-tests.js +318 -0
- package/dist/scripts/travel-timeline-tests.js +123 -0
- package/dist/scripts/unified-tests.js +49 -0
- package/dist/scripts/weather-tests.js +48 -0
- package/dist/src/bridge.js +34 -0
- package/dist/src/companions.js +112 -0
- package/dist/src/contracts.js +64 -0
- package/dist/src/dsh-llm.js +174 -0
- package/dist/src/engine.js +331 -0
- package/dist/src/index.js +1239 -0
- package/dist/src/journey.js +147 -0
- package/dist/src/loop.js +360 -0
- package/dist/src/memory-capture.js +40 -0
- package/dist/src/memory-decay.js +31 -0
- package/dist/src/memory-utility.js +55 -0
- package/dist/src/mock-llm.js +103 -0
- package/dist/src/model.js +134 -0
- package/dist/src/slot-spec.js +165 -0
- package/dist/src/state-ledger.js +901 -0
- package/dist/src/time-anchor.js +100 -0
- package/dist/src/tool-packet.js +12 -0
- package/dist/src/travel-slots.js +144 -0
- package/dist/src/travel-timeline.js +78 -0
- package/dist/src/unified.js +538 -0
- package/dist/src/wish-pool.js +27 -0
- package/package.json +84 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +158 -0
- package/ts/capabilities/anything.ts +201 -0
- package/ts/capabilities/flyai.ts +151 -0
- package/ts/capabilities/hbcli.ts +152 -0
- package/ts/capabilities/incident-log.ts +148 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +107 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/transport.ts +114 -0
- package/ts/capabilities/session-search.ts +129 -0
- package/ts/capabilities/weather.ts +176 -0
- package/ts/package.json +27 -0
- package/ts/scripts/async-collect.ts +50 -0
- package/ts/scripts/skeleton-check.ts +49 -0
- package/ts/scripts/skeleton-integration-test.ts +23 -0
- package/ts/scripts/state-cli.ts +201 -0
- package/ts/src/bridge.ts +44 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +165 -0
- package/ts/src/dsh-llm.ts +155 -0
- package/ts/src/index.ts +968 -0
- package/ts/src/loop.ts +411 -0
- package/ts/src/memory-capture.ts +59 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +100 -0
- package/ts/src/model.ts +220 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +849 -0
- package/ts/src/time-anchor.ts +134 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/unified.ts +582 -0
- package/ts/src/wish-pool.ts +66 -0
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
5
|
+
import { mergeProfile } from './memory-capture.js';
|
|
6
|
+
import { appendEvent as gateUtilityEvent, projectUtility } from './memory-utility.js';
|
|
7
|
+
import { appendTrip as gateTrip } from './travel-timeline.js';
|
|
8
|
+
import { upsertCompanion } from './companions.js';
|
|
9
|
+
const SCHEMA_VERSION = '1';
|
|
10
|
+
export function makeWishId(name) {
|
|
11
|
+
return 'w-' + createHash('sha256').update(String(name).trim()).digest('hex').slice(0, 10);
|
|
12
|
+
}
|
|
13
|
+
function sha(s) {
|
|
14
|
+
return createHash('sha256').update(s).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
function stateDirOf(stateRoot) {
|
|
17
|
+
const root = isAbsolute(stateRoot) ? stateRoot : join(process.cwd(), stateRoot);
|
|
18
|
+
return join(root, 'gotry-state');
|
|
19
|
+
}
|
|
20
|
+
export function ledgerDbPath(stateRoot) {
|
|
21
|
+
return join(stateDirOf(stateRoot), 'gotry-state.db');
|
|
22
|
+
}
|
|
23
|
+
export function ledgerExists(stateRoot) {
|
|
24
|
+
return existsSync(ledgerDbPath(stateRoot));
|
|
25
|
+
}
|
|
26
|
+
const LEGACY_FILES = [
|
|
27
|
+
'motivation-profile.json',
|
|
28
|
+
'wish-pool.json',
|
|
29
|
+
'memory-utility.jsonl',
|
|
30
|
+
'trips.jsonl',
|
|
31
|
+
'companions.json'
|
|
32
|
+
];
|
|
33
|
+
function legacyFilesPresent(stateRoot) {
|
|
34
|
+
const dir = stateDirOf(stateRoot);
|
|
35
|
+
return LEGACY_FILES.some((f)=>existsSync(join(dir, f)));
|
|
36
|
+
}
|
|
37
|
+
function isUniqueViolation(e) {
|
|
38
|
+
return e instanceof Error && e.code === 'SQLITE_CONSTRAINT_UNIQUE';
|
|
39
|
+
}
|
|
40
|
+
const SCHEMA = `
|
|
41
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
42
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
43
|
+
tenant_id TEXT NOT NULL DEFAULT 'local',
|
|
44
|
+
ts TEXT NOT NULL,
|
|
45
|
+
actor TEXT NOT NULL,
|
|
46
|
+
kind TEXT NOT NULL,
|
|
47
|
+
subject_id TEXT NOT NULL DEFAULT '',
|
|
48
|
+
payload TEXT NOT NULL,
|
|
49
|
+
idem_key TEXT,
|
|
50
|
+
run_id TEXT
|
|
51
|
+
);
|
|
52
|
+
CREATE UNIQUE INDEX IF NOT EXISTS events_idem ON events(tenant_id, idem_key) WHERE idem_key IS NOT NULL;
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS projection_docs (
|
|
55
|
+
tenant_id TEXT NOT NULL DEFAULT 'local',
|
|
56
|
+
subject TEXT NOT NULL,
|
|
57
|
+
doc TEXT NOT NULL,
|
|
58
|
+
PRIMARY KEY (tenant_id, subject)
|
|
59
|
+
);
|
|
60
|
+
CREATE TABLE IF NOT EXISTS projection_items (
|
|
61
|
+
tenant_id TEXT NOT NULL DEFAULT 'local',
|
|
62
|
+
subject TEXT NOT NULL,
|
|
63
|
+
item_id TEXT NOT NULL,
|
|
64
|
+
doc TEXT NOT NULL,
|
|
65
|
+
ord INTEGER NOT NULL DEFAULT 0,
|
|
66
|
+
PRIMARY KEY (tenant_id, subject, item_id)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
70
|
+
tenant_id TEXT NOT NULL DEFAULT 'local',
|
|
71
|
+
id TEXT NOT NULL,
|
|
72
|
+
goal TEXT NOT NULL,
|
|
73
|
+
status TEXT NOT NULL CHECK (status IN ('pending','settled','failed')),
|
|
74
|
+
ticket_json TEXT NOT NULL,
|
|
75
|
+
state_json TEXT NOT NULL,
|
|
76
|
+
deliverable TEXT,
|
|
77
|
+
created TEXT NOT NULL,
|
|
78
|
+
updated TEXT NOT NULL,
|
|
79
|
+
PRIMARY KEY (tenant_id, id)
|
|
80
|
+
);
|
|
81
|
+
CREATE TABLE IF NOT EXISTS workflow_steps (
|
|
82
|
+
tenant_id TEXT NOT NULL DEFAULT 'local',
|
|
83
|
+
run_id TEXT NOT NULL,
|
|
84
|
+
name TEXT NOT NULL,
|
|
85
|
+
status TEXT NOT NULL CHECK (status IN ('intent','done','failed')),
|
|
86
|
+
result TEXT,
|
|
87
|
+
intent_ts TEXT NOT NULL,
|
|
88
|
+
done_ts TEXT,
|
|
89
|
+
PRIMARY KEY (tenant_id, run_id, name)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
CREATE TABLE IF NOT EXISTS pending_writes (
|
|
93
|
+
tenant_id TEXT NOT NULL DEFAULT 'local',
|
|
94
|
+
idem_key TEXT NOT NULL,
|
|
95
|
+
seam TEXT NOT NULL,
|
|
96
|
+
payload TEXT NOT NULL,
|
|
97
|
+
status TEXT NOT NULL CHECK (status IN ('pending','confirmed','compensated')),
|
|
98
|
+
receipt TEXT,
|
|
99
|
+
created TEXT NOT NULL,
|
|
100
|
+
updated TEXT NOT NULL,
|
|
101
|
+
PRIMARY KEY (tenant_id, idem_key)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL);
|
|
105
|
+
`;
|
|
106
|
+
export class StateLedger {
|
|
107
|
+
db;
|
|
108
|
+
stateRoot;
|
|
109
|
+
tenant;
|
|
110
|
+
dbPath;
|
|
111
|
+
constructor(db, stateRoot, tenant = 'local'){
|
|
112
|
+
this.db = db;
|
|
113
|
+
this.stateRoot = stateRoot;
|
|
114
|
+
this.tenant = tenant;
|
|
115
|
+
this.dbPath = ledgerDbPath(stateRoot);
|
|
116
|
+
}
|
|
117
|
+
insertEvent(ev) {
|
|
118
|
+
const stmt = this.db.prepare(`INSERT INTO events (ts, actor, kind, subject_id, payload, idem_key, run_id)
|
|
119
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
120
|
+
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);
|
|
122
|
+
return Number(info.lastInsertRowid);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
if (isUniqueViolation(e)) return null;
|
|
125
|
+
throw e;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
readEvents(kind, limit = 100) {
|
|
129
|
+
const where = kind ? 'WHERE kind = ?' : '';
|
|
130
|
+
const rows = this.db.prepare(`SELECT seq, ts, actor, kind, subject_id, payload, idem_key, run_id
|
|
131
|
+
FROM events ${where} ORDER BY seq DESC LIMIT ?`).all(...kind ? [
|
|
132
|
+
kind,
|
|
133
|
+
limit
|
|
134
|
+
] : [
|
|
135
|
+
limit
|
|
136
|
+
]);
|
|
137
|
+
return rows;
|
|
138
|
+
}
|
|
139
|
+
countEvents() {
|
|
140
|
+
return this.db.prepare('SELECT COUNT(*) AS n FROM events WHERE tenant_id = ?').get(this.tenant).n;
|
|
141
|
+
}
|
|
142
|
+
readMotivation() {
|
|
143
|
+
const row = this.db.prepare(`SELECT doc FROM projection_docs WHERE subject = 'motivation' AND tenant_id = ?`).get(this.tenant);
|
|
144
|
+
return row ? JSON.parse(row.doc) : null;
|
|
145
|
+
}
|
|
146
|
+
readWishPool() {
|
|
147
|
+
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((r)=>JSON.parse(r.doc));
|
|
149
|
+
}
|
|
150
|
+
readCompanions() {
|
|
151
|
+
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((r)=>JSON.parse(r.doc));
|
|
153
|
+
}
|
|
154
|
+
readUtilityEvents() {
|
|
155
|
+
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((r)=>JSON.parse(r.payload).event);
|
|
157
|
+
}
|
|
158
|
+
readTrips() {
|
|
159
|
+
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((r)=>JSON.parse(r.payload).trip);
|
|
161
|
+
}
|
|
162
|
+
projectUtilityNow() {
|
|
163
|
+
return projectUtility(this.readUtilityEvents());
|
|
164
|
+
}
|
|
165
|
+
appendMotivationPatch(patch, actor = 'tool:gotry_motivation_save') {
|
|
166
|
+
const ts = new Date().toISOString();
|
|
167
|
+
const run = this.db.transaction(()=>{
|
|
168
|
+
const current = this.readMotivation();
|
|
169
|
+
const merged = mergeProfile(current, patch);
|
|
170
|
+
if (!merged) {
|
|
171
|
+
const unchanged = {
|
|
172
|
+
...current ?? {
|
|
173
|
+
weights: {},
|
|
174
|
+
evidence: [],
|
|
175
|
+
hard: {}
|
|
176
|
+
},
|
|
177
|
+
updated_at: current?.updated_at ?? ts
|
|
178
|
+
};
|
|
179
|
+
return {
|
|
180
|
+
saved: false,
|
|
181
|
+
profile: unchanged
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const doc = {
|
|
185
|
+
...merged,
|
|
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
|
+
});
|
|
197
|
+
this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
|
|
198
|
+
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
|
+
};
|
|
203
|
+
});
|
|
204
|
+
return run();
|
|
205
|
+
}
|
|
206
|
+
appendWish(entry, actor = 'tool:gotry_wish_pool_add') {
|
|
207
|
+
const name = String(entry.name ?? '').trim();
|
|
208
|
+
const conditions = entry.conditions;
|
|
209
|
+
if (!name || !conditions || typeof conditions !== 'object' || Object.keys(conditions).length === 0) {
|
|
210
|
+
throw new Error('wish pool entry requires name and conditions (fulfilment conditions are the whole point)');
|
|
211
|
+
}
|
|
212
|
+
const ts = new Date().toISOString();
|
|
213
|
+
const run = this.db.transaction(()=>{
|
|
214
|
+
const pool = this.readWishPool();
|
|
215
|
+
const existing = pool.find((w)=>String(w.name ?? '') === name);
|
|
216
|
+
if (existing) {
|
|
217
|
+
const id = typeof existing.wish_id === 'string' && existing.wish_id ? existing.wish_id : makeWishId(name);
|
|
218
|
+
const next = {
|
|
219
|
+
...existing,
|
|
220
|
+
wish_id: id,
|
|
221
|
+
reason: entry.reason !== undefined ? entry.reason : existing.reason,
|
|
222
|
+
conditions,
|
|
223
|
+
...entry.muted !== undefined ? {
|
|
224
|
+
muted: Boolean(entry.muted)
|
|
225
|
+
} : {}
|
|
226
|
+
};
|
|
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
|
+
});
|
|
243
|
+
this.upsertItem('wish_pool', id, next);
|
|
244
|
+
return {
|
|
245
|
+
added: false,
|
|
246
|
+
wish_id: id,
|
|
247
|
+
total: pool.length
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const id = makeWishId(name);
|
|
251
|
+
const doc = {
|
|
252
|
+
wish_id: id,
|
|
253
|
+
name,
|
|
254
|
+
reason: entry.reason ?? '',
|
|
255
|
+
conditions,
|
|
256
|
+
...entry.muted !== undefined ? {
|
|
257
|
+
muted: Boolean(entry.muted)
|
|
258
|
+
} : {},
|
|
259
|
+
added_at: ts
|
|
260
|
+
};
|
|
261
|
+
const seq = this.insertEvent({
|
|
262
|
+
actor,
|
|
263
|
+
kind: 'wish.added',
|
|
264
|
+
subjectId: id,
|
|
265
|
+
payload: {
|
|
266
|
+
wish: doc
|
|
267
|
+
},
|
|
268
|
+
ts
|
|
269
|
+
});
|
|
270
|
+
this.insertItem('wish_pool', id, doc, seq ?? 0);
|
|
271
|
+
return {
|
|
272
|
+
added: true,
|
|
273
|
+
wish_id: id,
|
|
274
|
+
total: pool.length + 1
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
return run();
|
|
278
|
+
}
|
|
279
|
+
appendUtilityEvent(ev, actor = 'tool:gotry_wish_pool_list') {
|
|
280
|
+
const full = gateUtilityEvent([], ev).events[0];
|
|
281
|
+
if (!full) return {
|
|
282
|
+
appended: false,
|
|
283
|
+
events: this.readUtilityEvents()
|
|
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;
|
|
295
|
+
});
|
|
296
|
+
return {
|
|
297
|
+
appended: run(),
|
|
298
|
+
events: this.readUtilityEvents()
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
appendTripEvent(ev, actor = 'tool:gotry_trip_log') {
|
|
302
|
+
const run = this.db.transaction(()=>{
|
|
303
|
+
const existing = this.readTrips();
|
|
304
|
+
const gate = gateTrip(existing, ev);
|
|
305
|
+
if (!gate.appended) return {
|
|
306
|
+
appended: false,
|
|
307
|
+
tripId: gate.tripId,
|
|
308
|
+
reason: gate.reason,
|
|
309
|
+
total: existing.length
|
|
310
|
+
};
|
|
311
|
+
const full = gate.events[gate.events.length - 1];
|
|
312
|
+
this.insertEvent({
|
|
313
|
+
actor,
|
|
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
|
+
};
|
|
326
|
+
});
|
|
327
|
+
return run();
|
|
328
|
+
}
|
|
329
|
+
appendCompanion(patch, actor = 'tool:gotry_companion_save') {
|
|
330
|
+
const run = this.db.transaction(()=>{
|
|
331
|
+
const profiles = this.readCompanions();
|
|
332
|
+
const res = upsertCompanion(profiles, patch);
|
|
333
|
+
if (!res.appended) return {
|
|
334
|
+
appended: false,
|
|
335
|
+
companionId: res.companionId,
|
|
336
|
+
reason: res.reason,
|
|
337
|
+
total: profiles.length
|
|
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
|
+
};
|
|
356
|
+
});
|
|
357
|
+
return run();
|
|
358
|
+
}
|
|
359
|
+
confirmOutcome(input, actor = 'tool:gotry_wish_pool_list') {
|
|
360
|
+
const run = this.db.transaction(()=>{
|
|
361
|
+
const now = new Date().toISOString();
|
|
362
|
+
const full = gateUtilityEvent([], {
|
|
363
|
+
wish_id: input.wishId,
|
|
364
|
+
kind: 'verified_outcome',
|
|
365
|
+
ts: now,
|
|
366
|
+
ctx: 'gotry_wish_pool_list.confirm',
|
|
367
|
+
detail: input.detail,
|
|
368
|
+
attribution: input.attribution
|
|
369
|
+
}).events[0];
|
|
370
|
+
const recorded = full ? this.insertEvent({
|
|
371
|
+
actor,
|
|
372
|
+
kind: 'memory_utility.event',
|
|
373
|
+
subjectId: full.wish_id,
|
|
374
|
+
payload: {
|
|
375
|
+
event: full
|
|
376
|
+
},
|
|
377
|
+
idemKey: `mu:${full.event_id}`
|
|
378
|
+
}) !== null : false;
|
|
379
|
+
let trip;
|
|
380
|
+
if (input.trip) {
|
|
381
|
+
const existing = this.readTrips();
|
|
382
|
+
const gate = gateTrip(existing, input.trip);
|
|
383
|
+
if (gate.appended) {
|
|
384
|
+
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
|
+
});
|
|
394
|
+
}
|
|
395
|
+
trip = {
|
|
396
|
+
appended: gate.appended,
|
|
397
|
+
tripId: gate.tripId,
|
|
398
|
+
reason: gate.reason
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
recorded,
|
|
403
|
+
trip
|
|
404
|
+
};
|
|
405
|
+
});
|
|
406
|
+
return run();
|
|
407
|
+
}
|
|
408
|
+
upsertItem(subject, itemId, doc) {
|
|
409
|
+
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
|
+
this.db.prepare(`INSERT INTO projection_items (tenant_id, subject, item_id, doc, ord) VALUES (?, ?, ?, ?, ?)
|
|
411
|
+
ON CONFLICT(tenant_id, subject, item_id) DO UPDATE SET doc = excluded.doc`).run(this.tenant, subject, itemId, JSON.stringify(doc), ord);
|
|
412
|
+
}
|
|
413
|
+
insertItem(subject, itemId, doc, ord) {
|
|
414
|
+
this.db.prepare(`INSERT INTO projection_items (tenant_id, subject, item_id, doc, ord) VALUES (?, ?, ?, ?, ?)
|
|
415
|
+
ON CONFLICT(tenant_id, subject, item_id) DO UPDATE SET doc = excluded.doc`).run(this.tenant, subject, itemId, JSON.stringify(doc), ord);
|
|
416
|
+
}
|
|
417
|
+
replaceItems(subject, items) {
|
|
418
|
+
this.db.prepare('DELETE FROM projection_items WHERE subject = ? AND tenant_id = ?').run(subject, this.tenant);
|
|
419
|
+
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));
|
|
421
|
+
}
|
|
422
|
+
nextOrd(subject) {
|
|
423
|
+
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
|
+
}
|
|
425
|
+
rebuildProjections(toSeq) {
|
|
426
|
+
const run = this.db.transaction(()=>{
|
|
427
|
+
this.db.prepare('DELETE FROM projection_docs WHERE tenant_id = ?').run(this.tenant);
|
|
428
|
+
this.db.prepare('DELETE FROM projection_items WHERE tenant_id = ?').run(this.tenant);
|
|
429
|
+
const rows = (toSeq === undefined ? this.db.prepare('SELECT seq, ts, kind, subject_id, payload FROM events WHERE tenant_id = ? ORDER BY seq') : this.db.prepare('SELECT seq, ts, kind, subject_id, payload FROM events WHERE seq <= ? AND tenant_id = ? ORDER BY seq')).all(...toSeq === undefined ? [
|
|
430
|
+
this.tenant
|
|
431
|
+
] : [
|
|
432
|
+
toSeq,
|
|
433
|
+
this.tenant
|
|
434
|
+
]);
|
|
435
|
+
for (const row of rows)this.foldEvent(row);
|
|
436
|
+
const wishes = this.db.prepare(`SELECT COUNT(*) AS n FROM projection_items WHERE subject = 'wish_pool' AND tenant_id = ?`).get(this.tenant).n;
|
|
437
|
+
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
|
+
};
|
|
443
|
+
});
|
|
444
|
+
return run();
|
|
445
|
+
}
|
|
446
|
+
foldEvent(row) {
|
|
447
|
+
const p = JSON.parse(row.payload);
|
|
448
|
+
switch(row.kind){
|
|
449
|
+
case 'motivation.imported':
|
|
450
|
+
{
|
|
451
|
+
const doc = p['profile'];
|
|
452
|
+
this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
|
|
453
|
+
ON CONFLICT(tenant_id, subject) DO UPDATE SET doc = excluded.doc`).run(this.tenant, JSON.stringify(doc));
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
case 'motivation.patch':
|
|
457
|
+
{
|
|
458
|
+
const current = this.readMotivation();
|
|
459
|
+
const merged = mergeProfile(current, p['patch']);
|
|
460
|
+
if (merged) {
|
|
461
|
+
const doc = {
|
|
462
|
+
...merged,
|
|
463
|
+
updated_at: row.ts
|
|
464
|
+
};
|
|
465
|
+
this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
|
|
466
|
+
ON CONFLICT(tenant_id, subject) DO UPDATE SET doc = excluded.doc`).run(this.tenant, JSON.stringify(doc));
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
case 'wish.imported':
|
|
471
|
+
case 'wish.added':
|
|
472
|
+
{
|
|
473
|
+
const wish = p['wish'] ?? p['profile'];
|
|
474
|
+
this.insertItem('wish_pool', String(wish.wish_id ?? row.subject_id), wish, row.seq);
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
case 'wish.updated':
|
|
478
|
+
{
|
|
479
|
+
const id = String(p['wish_id']);
|
|
480
|
+
const patch = p['patch'] ?? {};
|
|
481
|
+
const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' AND item_id = ?`).get(id);
|
|
482
|
+
if (rows) {
|
|
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;
|
|
506
|
+
}
|
|
507
|
+
default:
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
createWorkflowRun(input, actor = 'system:async-request') {
|
|
512
|
+
const ts = new Date().toISOString();
|
|
513
|
+
const run = this.db.transaction(()=>{
|
|
514
|
+
this.db.prepare(`INSERT INTO workflow_runs (tenant_id, id, goal, status, ticket_json, state_json, deliverable, created, updated)
|
|
515
|
+
VALUES (?, ?, ?, 'pending', ?, ?, NULL, ?, ?)
|
|
516
|
+
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
|
+
});
|
|
527
|
+
});
|
|
528
|
+
run();
|
|
529
|
+
}
|
|
530
|
+
getWorkflowRun(id) {
|
|
531
|
+
return this.db.prepare('SELECT * FROM workflow_runs WHERE id = ? AND tenant_id = ?').get(id, this.tenant);
|
|
532
|
+
}
|
|
533
|
+
pendingWorkflowRuns() {
|
|
534
|
+
return this.db.prepare(`SELECT id, goal FROM workflow_runs WHERE status = 'pending' AND tenant_id = ? ORDER BY created`).all(this.tenant);
|
|
535
|
+
}
|
|
536
|
+
getWorkflowStep(runId, name) {
|
|
537
|
+
return this.db.prepare('SELECT status, result FROM workflow_steps WHERE run_id = ? AND name = ? AND tenant_id = ?').get(runId, name, this.tenant);
|
|
538
|
+
}
|
|
539
|
+
markStepIntent(runId, name) {
|
|
540
|
+
this.db.prepare(`INSERT INTO workflow_steps (tenant_id, run_id, name, status, intent_ts) VALUES (?, ?, ?, 'intent', ?)
|
|
541
|
+
ON CONFLICT(tenant_id, run_id, name) DO NOTHING`).run(this.tenant, runId, name, new Date().toISOString());
|
|
542
|
+
}
|
|
543
|
+
markStepDone(runId, name, result) {
|
|
544
|
+
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
|
+
}
|
|
546
|
+
settleWorkflowRun(id, deliverable, actor = 'system:async-collect') {
|
|
547
|
+
const run = this.db.transaction(()=>{
|
|
548
|
+
this.db.prepare(`UPDATE workflow_runs SET status = 'settled', deliverable = ?, updated = ? WHERE id = ? AND tenant_id = ?`).run(deliverable, new Date().toISOString(), id, this.tenant);
|
|
549
|
+
this.insertEvent({
|
|
550
|
+
actor,
|
|
551
|
+
kind: 'async.settled',
|
|
552
|
+
subjectId: id,
|
|
553
|
+
payload: {
|
|
554
|
+
bytes: deliverable.length
|
|
555
|
+
},
|
|
556
|
+
runId: id
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
run();
|
|
560
|
+
}
|
|
561
|
+
requestPendingWrite(input, actor = 'system:writegate') {
|
|
562
|
+
const ts = new Date().toISOString();
|
|
563
|
+
const run = this.db.transaction(()=>{
|
|
564
|
+
const info = this.db.prepare(`INSERT INTO pending_writes (tenant_id, idem_key, seam, payload, status, created, updated)
|
|
565
|
+
VALUES (?, ?, ?, ?, 'pending', ?, ?) ON CONFLICT(tenant_id, idem_key) DO NOTHING`).run(this.tenant, input.idemKey, input.seam, JSON.stringify(input.payload ?? {}), ts, ts);
|
|
566
|
+
if (info.changes === 0) {
|
|
567
|
+
const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ? AND tenant_id = ?').get(input.idemKey, this.tenant);
|
|
568
|
+
return {
|
|
569
|
+
created: false,
|
|
570
|
+
status: row.status
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
this.insertEvent({
|
|
574
|
+
actor,
|
|
575
|
+
kind: 'write.pending',
|
|
576
|
+
subjectId: input.idemKey,
|
|
577
|
+
payload: {
|
|
578
|
+
seam: input.seam,
|
|
579
|
+
payload: input.payload
|
|
580
|
+
},
|
|
581
|
+
idemKey: `pw:${sha(input.idemKey)}:pending`
|
|
582
|
+
});
|
|
583
|
+
return {
|
|
584
|
+
created: true,
|
|
585
|
+
status: 'pending'
|
|
586
|
+
};
|
|
587
|
+
});
|
|
588
|
+
return run();
|
|
589
|
+
}
|
|
590
|
+
confirmPendingWrite(idemKey, receipt, actor = 'system:writegate') {
|
|
591
|
+
const run = this.db.transaction(()=>{
|
|
592
|
+
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);
|
|
593
|
+
if (info.changes === 0) {
|
|
594
|
+
const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ? AND tenant_id = ?').get(idemKey, this.tenant);
|
|
595
|
+
return {
|
|
596
|
+
ok: false,
|
|
597
|
+
status: row?.status ?? 'missing'
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
this.insertEvent({
|
|
601
|
+
actor,
|
|
602
|
+
kind: 'write.confirmed',
|
|
603
|
+
subjectId: idemKey,
|
|
604
|
+
payload: {
|
|
605
|
+
receipt
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
return {
|
|
609
|
+
ok: true,
|
|
610
|
+
status: 'confirmed'
|
|
611
|
+
};
|
|
612
|
+
});
|
|
613
|
+
return run();
|
|
614
|
+
}
|
|
615
|
+
compensatePendingWrite(idemKey, note, actor = 'system:writegate') {
|
|
616
|
+
const run = this.db.transaction(()=>{
|
|
617
|
+
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);
|
|
618
|
+
if (info.changes === 0) {
|
|
619
|
+
const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ? AND tenant_id = ?').get(idemKey, this.tenant);
|
|
620
|
+
return {
|
|
621
|
+
ok: false,
|
|
622
|
+
status: row?.status ?? 'missing'
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
this.insertEvent({
|
|
626
|
+
actor,
|
|
627
|
+
kind: 'write.compensated',
|
|
628
|
+
subjectId: idemKey,
|
|
629
|
+
payload: {
|
|
630
|
+
note
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
return {
|
|
634
|
+
ok: true,
|
|
635
|
+
status: 'compensated'
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
return run();
|
|
639
|
+
}
|
|
640
|
+
listPendingWrites() {
|
|
641
|
+
return this.db.prepare('SELECT idem_key, seam, status, receipt, created FROM pending_writes WHERE tenant_id = ? ORDER BY created').all(this.tenant);
|
|
642
|
+
}
|
|
643
|
+
forkWhatIf(destPath) {
|
|
644
|
+
this.db.prepare('VACUUM INTO ?').run(destPath);
|
|
645
|
+
return destPath;
|
|
646
|
+
}
|
|
647
|
+
forgetSubject(subjects, actor = 'system:state-cli') {
|
|
648
|
+
const run = this.db.transaction(()=>{
|
|
649
|
+
let deleted = 0;
|
|
650
|
+
for (const s of subjects){
|
|
651
|
+
const placeholders = s.kinds.map(()=>'?').join(',');
|
|
652
|
+
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);
|
|
653
|
+
deleted += info.changes;
|
|
654
|
+
}
|
|
655
|
+
this.insertEvent({
|
|
656
|
+
actor,
|
|
657
|
+
kind: 'forget.executed',
|
|
658
|
+
payload: {
|
|
659
|
+
subjects
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
this.rebuildProjections();
|
|
663
|
+
return {
|
|
664
|
+
deleted
|
|
665
|
+
};
|
|
666
|
+
});
|
|
667
|
+
return run();
|
|
668
|
+
}
|
|
669
|
+
close() {
|
|
670
|
+
this.db.close();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const openLedgers = new Map();
|
|
674
|
+
export function openDb(stateRoot, tenant = 'local') {
|
|
675
|
+
const path = ledgerDbPath(stateRoot);
|
|
676
|
+
const cacheKey = `${path}::${tenant}`;
|
|
677
|
+
const cached = openLedgers.get(cacheKey);
|
|
678
|
+
if (cached) return cached;
|
|
679
|
+
mkdirSync(dirname(path), {
|
|
680
|
+
recursive: true
|
|
681
|
+
});
|
|
682
|
+
const db = new Database(path);
|
|
683
|
+
db.pragma('journal_mode = WAL');
|
|
684
|
+
db.pragma('synchronous = NORMAL');
|
|
685
|
+
db.pragma('busy_timeout = 5000');
|
|
686
|
+
const version = db.pragma('user_version', {
|
|
687
|
+
simple: true
|
|
688
|
+
});
|
|
689
|
+
if (version < 2) {
|
|
690
|
+
const hasEventsTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='events'").get() !== undefined;
|
|
691
|
+
const eventsHasTenant = hasEventsTable && db.prepare("PRAGMA table_info(events)").all().some((c)=>c.name === 'tenant_id');
|
|
692
|
+
const isV1 = hasEventsTable && !eventsHasTenant;
|
|
693
|
+
db.transaction(()=>{
|
|
694
|
+
if (isV1) {
|
|
695
|
+
db.exec(`
|
|
696
|
+
ALTER TABLE events ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'local';
|
|
697
|
+
DROP INDEX IF EXISTS events_idem;
|
|
698
|
+
CREATE UNIQUE INDEX events_idem ON events(tenant_id, idem_key) WHERE idem_key IS NOT NULL;
|
|
699
|
+
DROP TABLE IF EXISTS projection_docs;
|
|
700
|
+
DROP TABLE IF EXISTS projection_items;
|
|
701
|
+
DROP TABLE IF EXISTS workflow_runs;
|
|
702
|
+
DROP TABLE IF EXISTS workflow_steps;
|
|
703
|
+
DROP TABLE IF EXISTS pending_writes;
|
|
704
|
+
`);
|
|
705
|
+
} else {
|
|
706
|
+
const wrHasTenant = db.prepare("PRAGMA table_info(workflow_runs)").all().some((c)=>c.name === 'tenant_id');
|
|
707
|
+
if (!wrHasTenant && db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='workflow_runs'").get()) {
|
|
708
|
+
db.exec(`
|
|
709
|
+
DROP TABLE IF EXISTS workflow_runs;
|
|
710
|
+
DROP TABLE IF EXISTS workflow_steps;
|
|
711
|
+
DROP TABLE IF EXISTS pending_writes;
|
|
712
|
+
DROP TABLE IF EXISTS projection_docs;
|
|
713
|
+
DROP TABLE IF EXISTS projection_items;
|
|
714
|
+
`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
db.exec(SCHEMA);
|
|
718
|
+
db.pragma('user_version = 2');
|
|
719
|
+
db.prepare('INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v').run('schema_version', '2');
|
|
720
|
+
})();
|
|
721
|
+
if (isV1) {
|
|
722
|
+
const ledger = new StateLedger(db, stateRoot, tenant);
|
|
723
|
+
ledger.rebuildProjections();
|
|
724
|
+
}
|
|
725
|
+
} else {
|
|
726
|
+
db.exec(SCHEMA);
|
|
727
|
+
db.prepare('INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v').run('schema_version', '2');
|
|
728
|
+
}
|
|
729
|
+
const ledger = new StateLedger(db, stateRoot, tenant);
|
|
730
|
+
openLedgers.set(cacheKey, ledger);
|
|
731
|
+
return ledger;
|
|
732
|
+
}
|
|
733
|
+
export function openLedgerIfExists(stateRoot, tenant = 'local') {
|
|
734
|
+
if (!ledgerExists(stateRoot)) return null;
|
|
735
|
+
return openDb(stateRoot, tenant);
|
|
736
|
+
}
|
|
737
|
+
export function ensureLedger(stateRoot, tenant = 'local') {
|
|
738
|
+
if (ledgerExists(stateRoot)) return openDb(stateRoot, tenant);
|
|
739
|
+
const dir = stateDirOf(stateRoot);
|
|
740
|
+
const hasLegacy = legacyFilesPresent(stateRoot);
|
|
741
|
+
let backupDir = null;
|
|
742
|
+
if (hasLegacy) {
|
|
743
|
+
backupDir = join(dir, 'pre-ledger-backup');
|
|
744
|
+
mkdirSync(backupDir, {
|
|
745
|
+
recursive: true
|
|
746
|
+
});
|
|
747
|
+
for (const f of LEGACY_FILES){
|
|
748
|
+
if (existsSync(join(dir, f))) copyFileSync(join(dir, f), join(backupDir, f));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const ledger = openDb(stateRoot, tenant);
|
|
752
|
+
if (hasLegacy) importLegacyInto(ledger, dir);
|
|
753
|
+
return ledger;
|
|
754
|
+
}
|
|
755
|
+
function importLegacyInto(ledger, dir) {
|
|
756
|
+
const imported = {
|
|
757
|
+
motivation: false,
|
|
758
|
+
wishes: 0,
|
|
759
|
+
utilityEvents: 0,
|
|
760
|
+
trips: 0,
|
|
761
|
+
companions: 0
|
|
762
|
+
};
|
|
763
|
+
const run = ledger.db.transaction(()=>{
|
|
764
|
+
const flag = ledger.db.prepare(`SELECT v FROM kv WHERE k = 'legacy_imported_v1'`).get();
|
|
765
|
+
if (flag) return;
|
|
766
|
+
const readJsonSafe = (p)=>{
|
|
767
|
+
try {
|
|
768
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
769
|
+
} catch {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
const profile = readJsonSafe(join(dir, 'motivation-profile.json'));
|
|
774
|
+
if (profile) {
|
|
775
|
+
if (ledger.insertEvent({
|
|
776
|
+
actor: 'system:migrate',
|
|
777
|
+
kind: 'motivation.imported',
|
|
778
|
+
subjectId: 'motivation',
|
|
779
|
+
payload: {
|
|
780
|
+
profile
|
|
781
|
+
},
|
|
782
|
+
idemKey: 'import:motivation'
|
|
783
|
+
}) !== null) imported.motivation = true;
|
|
784
|
+
}
|
|
785
|
+
const pool = readJsonSafe(join(dir, 'wish-pool.json'));
|
|
786
|
+
if (Array.isArray(pool)) {
|
|
787
|
+
for (const w of pool){
|
|
788
|
+
const id = String(w['wish_id'] ?? makeWishId(String(w['name'] ?? '')));
|
|
789
|
+
const wish = {
|
|
790
|
+
...w,
|
|
791
|
+
wish_id: id
|
|
792
|
+
};
|
|
793
|
+
if (ledger.insertEvent({
|
|
794
|
+
actor: 'system:migrate',
|
|
795
|
+
kind: 'wish.imported',
|
|
796
|
+
subjectId: id,
|
|
797
|
+
payload: {
|
|
798
|
+
wish
|
|
799
|
+
},
|
|
800
|
+
idemKey: `import:wish:${id}`
|
|
801
|
+
}) !== null) imported.wishes++;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
try {
|
|
805
|
+
const lines = readFileSync(join(dir, 'memory-utility.jsonl'), 'utf-8').split('\n').filter(Boolean);
|
|
806
|
+
for (const line of lines){
|
|
807
|
+
try {
|
|
808
|
+
const ev = JSON.parse(line);
|
|
809
|
+
if (ledger.insertEvent({
|
|
810
|
+
actor: 'system:migrate',
|
|
811
|
+
kind: 'memory_utility.event',
|
|
812
|
+
subjectId: ev.wish_id,
|
|
813
|
+
payload: {
|
|
814
|
+
event: ev
|
|
815
|
+
},
|
|
816
|
+
idemKey: `mu:${ev.event_id}`
|
|
817
|
+
}) !== null) imported.utilityEvents++;
|
|
818
|
+
} catch {}
|
|
819
|
+
}
|
|
820
|
+
} catch {}
|
|
821
|
+
try {
|
|
822
|
+
const lines = readFileSync(join(dir, 'trips.jsonl'), 'utf-8').split('\n').filter(Boolean);
|
|
823
|
+
for (const line of lines){
|
|
824
|
+
try {
|
|
825
|
+
const trip = JSON.parse(line);
|
|
826
|
+
if (ledger.insertEvent({
|
|
827
|
+
actor: 'system:migrate',
|
|
828
|
+
kind: 'trip.logged',
|
|
829
|
+
subjectId: trip.trip_id,
|
|
830
|
+
payload: {
|
|
831
|
+
trip
|
|
832
|
+
},
|
|
833
|
+
idemKey: `trip:${trip.trip_id}`
|
|
834
|
+
}) !== null) imported.trips++;
|
|
835
|
+
} catch {}
|
|
836
|
+
}
|
|
837
|
+
} catch {}
|
|
838
|
+
const companions = readJsonSafe(join(dir, 'companions.json'));
|
|
839
|
+
if (Array.isArray(companions)) {
|
|
840
|
+
for (const c of companions){
|
|
841
|
+
const id = String(c['companion_id'] ?? '');
|
|
842
|
+
if (!id) continue;
|
|
843
|
+
if (ledger.insertEvent({
|
|
844
|
+
actor: 'system:migrate',
|
|
845
|
+
kind: 'companion.imported',
|
|
846
|
+
subjectId: id,
|
|
847
|
+
payload: {
|
|
848
|
+
profile: c
|
|
849
|
+
},
|
|
850
|
+
idemKey: `import:companion:${id}`
|
|
851
|
+
}) !== null) imported.companions++;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
ledger.rebuildProjections();
|
|
855
|
+
ledger.db.prepare(`INSERT INTO kv (k, v) VALUES ('legacy_imported_v1', 'done') ON CONFLICT(k) DO UPDATE SET v = excluded.v`).run();
|
|
856
|
+
});
|
|
857
|
+
run();
|
|
858
|
+
return imported;
|
|
859
|
+
}
|
|
860
|
+
function readLegacyJson(stateRoot, name, fallback) {
|
|
861
|
+
try {
|
|
862
|
+
return JSON.parse(readFileSync(join(stateDirOf(stateRoot), name), 'utf-8'));
|
|
863
|
+
} catch {
|
|
864
|
+
return fallback;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function readLegacyJsonl(stateRoot, name) {
|
|
868
|
+
try {
|
|
869
|
+
return readFileSync(join(stateDirOf(stateRoot), name), 'utf-8').split('\n').filter(Boolean).map((l)=>JSON.parse(l));
|
|
870
|
+
} catch {
|
|
871
|
+
return [];
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
export function readMotivationWithFallback(stateRoot) {
|
|
875
|
+
const ledger = openLedgerIfExists(stateRoot);
|
|
876
|
+
if (ledger) return ledger.readMotivation();
|
|
877
|
+
return readLegacyJson(stateRoot, 'motivation-profile.json', null);
|
|
878
|
+
}
|
|
879
|
+
export function readWishPoolWithFallback(stateRoot) {
|
|
880
|
+
const ledger = openLedgerIfExists(stateRoot);
|
|
881
|
+
if (ledger) return ledger.readWishPool();
|
|
882
|
+
return readLegacyJson(stateRoot, 'wish-pool.json', []);
|
|
883
|
+
}
|
|
884
|
+
export function readUtilityEventsWithFallback(stateRoot) {
|
|
885
|
+
const ledger = openLedgerIfExists(stateRoot);
|
|
886
|
+
if (ledger) return ledger.readUtilityEvents();
|
|
887
|
+
return readLegacyJsonl(stateRoot, 'memory-utility.jsonl');
|
|
888
|
+
}
|
|
889
|
+
export function readTripsWithFallback(stateRoot) {
|
|
890
|
+
const ledger = openLedgerIfExists(stateRoot);
|
|
891
|
+
if (ledger) return ledger.readTrips();
|
|
892
|
+
return readLegacyJsonl(stateRoot, 'trips.jsonl');
|
|
893
|
+
}
|
|
894
|
+
export function readCompanionsWithFallback(stateRoot) {
|
|
895
|
+
const ledger = openLedgerIfExists(stateRoot);
|
|
896
|
+
if (ledger) return ledger.readCompanions();
|
|
897
|
+
return readLegacyJson(stateRoot, 'companions.json', []);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/state-ledger.ts
|