@danceiny/gotry 0.0.1-rc.8 → 0.0.1-rc.9

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.
Files changed (41) hide show
  1. package/README.md +6 -4
  2. package/cordis.gotry-patch.yml +7 -0
  3. package/dist/capabilities/flyai.js +131 -0
  4. package/dist/capabilities/session/action-cache.js +120 -0
  5. package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
  6. package/dist/capabilities/session/adapters/meituan-local.js +73 -0
  7. package/dist/capabilities/session/extract.js +40 -0
  8. package/dist/capabilities/session/read-guard.js +56 -0
  9. package/dist/capabilities/session/transport.js +95 -0
  10. package/dist/capabilities/session-search.js +95 -0
  11. package/dist/scripts/action-cache-tests.js +107 -0
  12. package/dist/scripts/async-collect.js +26 -7
  13. package/dist/scripts/companion-tests.js +112 -0
  14. package/dist/scripts/ledger-tests.js +344 -0
  15. package/dist/scripts/ledger-workflow-crash.js +42 -0
  16. package/dist/scripts/memory-decay-tests.js +83 -0
  17. package/dist/scripts/memory-metrics.js +6 -20
  18. package/dist/scripts/nudge-digest.js +4 -14
  19. package/dist/scripts/session-attach-diagnose.js +31 -0
  20. package/dist/scripts/session-attach-poc.js +65 -0
  21. package/dist/scripts/session-attach-wait.js +41 -0
  22. package/dist/scripts/session-extract-tests.js +81 -0
  23. package/dist/scripts/session-login.js +48 -0
  24. package/dist/scripts/session-tests.js +162 -0
  25. package/dist/scripts/smoke.js +41 -1
  26. package/dist/scripts/state-cli-tests.js +136 -0
  27. package/dist/scripts/state-cli.js +234 -0
  28. package/dist/scripts/travel-timeline-tests.js +123 -0
  29. package/dist/scripts/unified-tests.js +1 -1
  30. package/dist/src/companions.js +112 -0
  31. package/dist/src/index.js +346 -117
  32. package/dist/src/loop.js +52 -15
  33. package/dist/src/memory-decay.js +31 -0
  34. package/dist/src/state-ledger.js +848 -0
  35. package/dist/src/travel-timeline.js +78 -0
  36. package/dist/src/unified.js +3 -1
  37. package/package.json +1 -1
  38. package/ts/package.json +3 -0
  39. package/ts/src/index.ts +216 -83
  40. package/ts/src/loop.ts +57 -17
  41. package/ts/src/unified.ts +6 -1
@@ -0,0 +1,848 @@
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
+ ts TEXT NOT NULL,
44
+ actor TEXT NOT NULL,
45
+ kind TEXT NOT NULL,
46
+ subject_id TEXT NOT NULL DEFAULT '',
47
+ payload TEXT NOT NULL,
48
+ idem_key TEXT,
49
+ run_id TEXT
50
+ );
51
+ CREATE UNIQUE INDEX IF NOT EXISTS events_idem ON events(idem_key) WHERE idem_key IS NOT NULL;
52
+
53
+ CREATE TABLE IF NOT EXISTS projection_docs (
54
+ subject TEXT PRIMARY KEY,
55
+ doc TEXT NOT NULL
56
+ );
57
+ CREATE TABLE IF NOT EXISTS projection_items (
58
+ subject TEXT NOT NULL,
59
+ item_id TEXT NOT NULL,
60
+ doc TEXT NOT NULL,
61
+ ord INTEGER NOT NULL DEFAULT 0,
62
+ PRIMARY KEY (subject, item_id)
63
+ );
64
+
65
+ CREATE TABLE IF NOT EXISTS workflow_runs (
66
+ id TEXT PRIMARY KEY,
67
+ goal TEXT NOT NULL,
68
+ status TEXT NOT NULL CHECK (status IN ('pending','settled','failed')),
69
+ ticket_json TEXT NOT NULL,
70
+ state_json TEXT NOT NULL,
71
+ deliverable TEXT,
72
+ created TEXT NOT NULL,
73
+ updated TEXT NOT NULL
74
+ );
75
+ CREATE TABLE IF NOT EXISTS workflow_steps (
76
+ run_id TEXT NOT NULL,
77
+ name TEXT NOT NULL,
78
+ status TEXT NOT NULL CHECK (status IN ('intent','done','failed')),
79
+ result TEXT,
80
+ intent_ts TEXT NOT NULL,
81
+ done_ts TEXT,
82
+ PRIMARY KEY (run_id, name)
83
+ );
84
+
85
+ CREATE TABLE IF NOT EXISTS pending_writes (
86
+ idem_key TEXT PRIMARY KEY,
87
+ seam TEXT NOT NULL,
88
+ payload TEXT NOT NULL,
89
+ status TEXT NOT NULL CHECK (status IN ('pending','confirmed','compensated')),
90
+ receipt TEXT,
91
+ created TEXT NOT NULL,
92
+ updated TEXT NOT NULL
93
+ );
94
+
95
+ CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL);
96
+ `;
97
+ export class StateLedger {
98
+ db;
99
+ stateRoot;
100
+ dbPath;
101
+ constructor(db, stateRoot){
102
+ this.db = db;
103
+ this.stateRoot = stateRoot;
104
+ this.dbPath = ledgerDbPath(stateRoot);
105
+ }
106
+ insertEvent(ev) {
107
+ const stmt = this.db.prepare(`INSERT INTO events (ts, actor, kind, subject_id, payload, idem_key, run_id)
108
+ VALUES (?, ?, ?, ?, ?, ?, ?)`);
109
+ try {
110
+ 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);
111
+ return Number(info.lastInsertRowid);
112
+ } catch (e) {
113
+ if (isUniqueViolation(e)) return null;
114
+ throw e;
115
+ }
116
+ }
117
+ readEvents(kind, limit = 100) {
118
+ const where = kind ? 'WHERE kind = ?' : '';
119
+ const rows = this.db.prepare(`SELECT seq, ts, actor, kind, subject_id, payload, idem_key, run_id
120
+ FROM events ${where} ORDER BY seq DESC LIMIT ?`).all(...kind ? [
121
+ kind,
122
+ limit
123
+ ] : [
124
+ limit
125
+ ]);
126
+ return rows;
127
+ }
128
+ countEvents() {
129
+ return this.db.prepare('SELECT COUNT(*) AS n FROM events').get().n;
130
+ }
131
+ readMotivation() {
132
+ const row = this.db.prepare(`SELECT doc FROM projection_docs WHERE subject = 'motivation'`).get();
133
+ return row ? JSON.parse(row.doc) : null;
134
+ }
135
+ readWishPool() {
136
+ const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' ORDER BY ord, rowid`).all();
137
+ return rows.map((r)=>JSON.parse(r.doc));
138
+ }
139
+ readCompanions() {
140
+ const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'companions' ORDER BY ord, rowid`).all();
141
+ return rows.map((r)=>JSON.parse(r.doc));
142
+ }
143
+ readUtilityEvents() {
144
+ const rows = this.db.prepare(`SELECT payload FROM events WHERE kind = 'memory_utility.event' ORDER BY seq`).all();
145
+ return rows.map((r)=>JSON.parse(r.payload).event);
146
+ }
147
+ readTrips() {
148
+ const rows = this.db.prepare(`SELECT payload FROM events WHERE kind = 'trip.logged' ORDER BY seq`).all();
149
+ return rows.map((r)=>JSON.parse(r.payload).trip);
150
+ }
151
+ projectUtilityNow() {
152
+ return projectUtility(this.readUtilityEvents());
153
+ }
154
+ appendMotivationPatch(patch, actor = 'tool:gotry_motivation_save') {
155
+ const ts = new Date().toISOString();
156
+ const run = this.db.transaction(()=>{
157
+ const current = this.readMotivation();
158
+ const merged = mergeProfile(current, patch);
159
+ if (!merged) {
160
+ const unchanged = {
161
+ ...current ?? {
162
+ weights: {},
163
+ evidence: [],
164
+ hard: {}
165
+ },
166
+ updated_at: current?.updated_at ?? ts
167
+ };
168
+ return {
169
+ saved: false,
170
+ profile: unchanged
171
+ };
172
+ }
173
+ const doc = {
174
+ ...merged,
175
+ updated_at: ts
176
+ };
177
+ this.insertEvent({
178
+ actor,
179
+ kind: 'motivation.patch',
180
+ subjectId: 'motivation',
181
+ payload: {
182
+ patch
183
+ },
184
+ ts
185
+ });
186
+ this.db.prepare(`INSERT INTO projection_docs (subject, doc) VALUES ('motivation', ?)
187
+ ON CONFLICT(subject) DO UPDATE SET doc = excluded.doc`).run(JSON.stringify(doc));
188
+ return {
189
+ saved: true,
190
+ profile: doc
191
+ };
192
+ });
193
+ return run();
194
+ }
195
+ appendWish(entry, actor = 'tool:gotry_wish_pool_add') {
196
+ const name = String(entry.name ?? '').trim();
197
+ const conditions = entry.conditions;
198
+ if (!name || !conditions || typeof conditions !== 'object' || Object.keys(conditions).length === 0) {
199
+ throw new Error('wish pool entry requires name and conditions (fulfilment conditions are the whole point)');
200
+ }
201
+ const ts = new Date().toISOString();
202
+ const run = this.db.transaction(()=>{
203
+ const pool = this.readWishPool();
204
+ const existing = pool.find((w)=>String(w.name ?? '') === name);
205
+ if (existing) {
206
+ const id = typeof existing.wish_id === 'string' && existing.wish_id ? existing.wish_id : makeWishId(name);
207
+ const next = {
208
+ ...existing,
209
+ wish_id: id,
210
+ reason: entry.reason !== undefined ? entry.reason : existing.reason,
211
+ conditions,
212
+ ...entry.muted !== undefined ? {
213
+ muted: Boolean(entry.muted)
214
+ } : {}
215
+ };
216
+ this.insertEvent({
217
+ actor,
218
+ kind: 'wish.updated',
219
+ subjectId: id,
220
+ payload: {
221
+ wish_id: id,
222
+ patch: {
223
+ reason: next.reason,
224
+ conditions,
225
+ ...entry.muted !== undefined ? {
226
+ muted: Boolean(entry.muted)
227
+ } : {}
228
+ }
229
+ },
230
+ ts
231
+ });
232
+ this.upsertItem('wish_pool', id, next);
233
+ return {
234
+ added: false,
235
+ wish_id: id,
236
+ total: pool.length
237
+ };
238
+ }
239
+ const id = makeWishId(name);
240
+ const doc = {
241
+ wish_id: id,
242
+ name,
243
+ reason: entry.reason ?? '',
244
+ conditions,
245
+ ...entry.muted !== undefined ? {
246
+ muted: Boolean(entry.muted)
247
+ } : {},
248
+ added_at: ts
249
+ };
250
+ const seq = this.insertEvent({
251
+ actor,
252
+ kind: 'wish.added',
253
+ subjectId: id,
254
+ payload: {
255
+ wish: doc
256
+ },
257
+ ts
258
+ });
259
+ this.insertItem('wish_pool', id, doc, seq ?? 0);
260
+ return {
261
+ added: true,
262
+ wish_id: id,
263
+ total: pool.length + 1
264
+ };
265
+ });
266
+ return run();
267
+ }
268
+ appendUtilityEvent(ev, actor = 'tool:gotry_wish_pool_list') {
269
+ const full = gateUtilityEvent([], ev).events[0];
270
+ if (!full) return {
271
+ appended: false,
272
+ events: this.readUtilityEvents()
273
+ };
274
+ const run = this.db.transaction(()=>{
275
+ return this.insertEvent({
276
+ actor,
277
+ kind: 'memory_utility.event',
278
+ subjectId: full.wish_id,
279
+ payload: {
280
+ event: full
281
+ },
282
+ idemKey: `mu:${full.event_id}`
283
+ }) !== null;
284
+ });
285
+ return {
286
+ appended: run(),
287
+ events: this.readUtilityEvents()
288
+ };
289
+ }
290
+ appendTripEvent(ev, actor = 'tool:gotry_trip_log') {
291
+ const run = this.db.transaction(()=>{
292
+ const existing = this.readTrips();
293
+ const gate = gateTrip(existing, ev);
294
+ if (!gate.appended) return {
295
+ appended: false,
296
+ tripId: gate.tripId,
297
+ reason: gate.reason,
298
+ total: existing.length
299
+ };
300
+ const full = gate.events[gate.events.length - 1];
301
+ this.insertEvent({
302
+ actor,
303
+ kind: 'trip.logged',
304
+ subjectId: full.trip_id,
305
+ payload: {
306
+ trip: full
307
+ },
308
+ idemKey: `trip:${full.trip_id}`
309
+ });
310
+ return {
311
+ appended: true,
312
+ tripId: full.trip_id,
313
+ total: gate.events.length
314
+ };
315
+ });
316
+ return run();
317
+ }
318
+ appendCompanion(patch, actor = 'tool:gotry_companion_save') {
319
+ const run = this.db.transaction(()=>{
320
+ const profiles = this.readCompanions();
321
+ const res = upsertCompanion(profiles, patch);
322
+ if (!res.appended) return {
323
+ appended: false,
324
+ companionId: res.companionId,
325
+ reason: res.reason,
326
+ total: profiles.length
327
+ };
328
+ this.insertEvent({
329
+ actor,
330
+ kind: 'companion.saved',
331
+ subjectId: res.companionId,
332
+ payload: {
333
+ patch
334
+ }
335
+ });
336
+ this.replaceItems('companions', res.profiles.map((p)=>[
337
+ p.companion_id,
338
+ p
339
+ ]));
340
+ return {
341
+ appended: true,
342
+ companionId: res.companionId,
343
+ total: res.profiles.length
344
+ };
345
+ });
346
+ return run();
347
+ }
348
+ confirmOutcome(input, actor = 'tool:gotry_wish_pool_list') {
349
+ const run = this.db.transaction(()=>{
350
+ const now = new Date().toISOString();
351
+ const full = gateUtilityEvent([], {
352
+ wish_id: input.wishId,
353
+ kind: 'verified_outcome',
354
+ ts: now,
355
+ ctx: 'gotry_wish_pool_list.confirm',
356
+ detail: input.detail,
357
+ attribution: input.attribution
358
+ }).events[0];
359
+ const recorded = full ? this.insertEvent({
360
+ actor,
361
+ kind: 'memory_utility.event',
362
+ subjectId: full.wish_id,
363
+ payload: {
364
+ event: full
365
+ },
366
+ idemKey: `mu:${full.event_id}`
367
+ }) !== null : false;
368
+ let trip;
369
+ if (input.trip) {
370
+ const existing = this.readTrips();
371
+ const gate = gateTrip(existing, input.trip);
372
+ if (gate.appended) {
373
+ const fullTrip = gate.events[gate.events.length - 1];
374
+ this.insertEvent({
375
+ actor,
376
+ kind: 'trip.logged',
377
+ subjectId: fullTrip.trip_id,
378
+ payload: {
379
+ trip: fullTrip
380
+ },
381
+ idemKey: `trip:${fullTrip.trip_id}`
382
+ });
383
+ }
384
+ trip = {
385
+ appended: gate.appended,
386
+ tripId: gate.tripId,
387
+ reason: gate.reason
388
+ };
389
+ }
390
+ return {
391
+ recorded,
392
+ trip
393
+ };
394
+ });
395
+ return run();
396
+ }
397
+ upsertItem(subject, itemId, doc) {
398
+ const ord = this.db.prepare(`SELECT ord FROM projection_items WHERE subject = ? AND item_id = ?`).get(subject, itemId)?.ord ?? this.nextOrd(subject);
399
+ this.db.prepare(`INSERT INTO projection_items (subject, item_id, doc, ord) VALUES (?, ?, ?, ?)
400
+ ON CONFLICT(subject, item_id) DO UPDATE SET doc = excluded.doc`).run(subject, itemId, JSON.stringify(doc), ord);
401
+ }
402
+ insertItem(subject, itemId, doc, ord) {
403
+ this.db.prepare(`INSERT INTO projection_items (subject, item_id, doc, ord) VALUES (?, ?, ?, ?)
404
+ ON CONFLICT(subject, item_id) DO UPDATE SET doc = excluded.doc`).run(subject, itemId, JSON.stringify(doc), ord);
405
+ }
406
+ replaceItems(subject, items) {
407
+ this.db.prepare('DELETE FROM projection_items WHERE subject = ?').run(subject);
408
+ const ins = this.db.prepare('INSERT INTO projection_items (subject, item_id, doc, ord) VALUES (?, ?, ?, ?)');
409
+ items.forEach(([id, doc], i)=>ins.run(subject, id, JSON.stringify(doc), i));
410
+ }
411
+ nextOrd(subject) {
412
+ return (this.db.prepare('SELECT COALESCE(MAX(ord), -1) AS m FROM projection_items WHERE subject = ?').get(subject).m ?? -1) + 1;
413
+ }
414
+ rebuildProjections(toSeq) {
415
+ const run = this.db.transaction(()=>{
416
+ this.db.prepare('DELETE FROM projection_docs').run();
417
+ this.db.prepare('DELETE FROM projection_items').run();
418
+ const rows = (toSeq === undefined ? this.db.prepare('SELECT seq, ts, kind, subject_id, payload FROM events ORDER BY seq') : this.db.prepare('SELECT seq, ts, kind, subject_id, payload FROM events WHERE seq <= ? ORDER BY seq')).all(...toSeq === undefined ? [] : [
419
+ toSeq
420
+ ]);
421
+ for (const row of rows)this.foldEvent(row);
422
+ const wishes = this.db.prepare(`SELECT COUNT(*) AS n FROM projection_items WHERE subject = 'wish_pool'`).get().n;
423
+ const companions = this.db.prepare(`SELECT COUNT(*) AS n FROM projection_items WHERE subject = 'companions'`).get().n;
424
+ return {
425
+ events: rows.length,
426
+ wishes,
427
+ companions
428
+ };
429
+ });
430
+ return run();
431
+ }
432
+ foldEvent(row) {
433
+ const p = JSON.parse(row.payload);
434
+ switch(row.kind){
435
+ case 'motivation.imported':
436
+ {
437
+ const doc = p['profile'];
438
+ this.db.prepare(`INSERT INTO projection_docs (subject, doc) VALUES ('motivation', ?)
439
+ ON CONFLICT(subject) DO UPDATE SET doc = excluded.doc`).run(JSON.stringify(doc));
440
+ break;
441
+ }
442
+ case 'motivation.patch':
443
+ {
444
+ const current = this.readMotivation();
445
+ const merged = mergeProfile(current, p['patch']);
446
+ if (merged) {
447
+ const doc = {
448
+ ...merged,
449
+ updated_at: row.ts
450
+ };
451
+ this.db.prepare(`INSERT INTO projection_docs (subject, doc) VALUES ('motivation', ?)
452
+ ON CONFLICT(subject) DO UPDATE SET doc = excluded.doc`).run(JSON.stringify(doc));
453
+ }
454
+ break;
455
+ }
456
+ case 'wish.imported':
457
+ case 'wish.added':
458
+ {
459
+ const wish = p['wish'] ?? p['profile'];
460
+ this.insertItem('wish_pool', String(wish.wish_id ?? row.subject_id), wish, row.seq);
461
+ break;
462
+ }
463
+ case 'wish.updated':
464
+ {
465
+ const id = String(p['wish_id']);
466
+ const patch = p['patch'] ?? {};
467
+ const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' AND item_id = ?`).get(id);
468
+ if (rows) {
469
+ const doc = {
470
+ ...JSON.parse(rows.doc),
471
+ ...patch
472
+ };
473
+ this.upsertItem('wish_pool', id, doc);
474
+ }
475
+ break;
476
+ }
477
+ case 'companion.imported':
478
+ {
479
+ const profile = p['profile'];
480
+ this.insertItem('companions', profile.companion_id, profile, row.seq);
481
+ break;
482
+ }
483
+ case 'companion.saved':
484
+ {
485
+ const patch = p['patch'];
486
+ const res = upsertCompanion(this.readCompanions(), patch);
487
+ this.replaceItems('companions', res.profiles.map((c)=>[
488
+ c.companion_id,
489
+ c
490
+ ]));
491
+ break;
492
+ }
493
+ default:
494
+ break;
495
+ }
496
+ }
497
+ createWorkflowRun(input, actor = 'system:async-request') {
498
+ const ts = new Date().toISOString();
499
+ const run = this.db.transaction(()=>{
500
+ this.db.prepare(`INSERT INTO workflow_runs (id, goal, status, ticket_json, state_json, deliverable, created, updated)
501
+ VALUES (?, ?, 'pending', ?, ?, NULL, ?, ?)
502
+ ON CONFLICT(id) DO UPDATE SET updated = excluded.updated`).run(input.id, input.goal, JSON.stringify(input.ticket), JSON.stringify(input.state), ts, ts);
503
+ this.insertEvent({
504
+ actor,
505
+ kind: 'async.run_created',
506
+ subjectId: input.id,
507
+ payload: {
508
+ ticket: input.ticket
509
+ },
510
+ runId: input.id,
511
+ ts
512
+ });
513
+ });
514
+ run();
515
+ }
516
+ getWorkflowRun(id) {
517
+ return this.db.prepare('SELECT * FROM workflow_runs WHERE id = ?').get(id);
518
+ }
519
+ pendingWorkflowRuns() {
520
+ return this.db.prepare(`SELECT id, goal FROM workflow_runs WHERE status = 'pending' ORDER BY created`).all();
521
+ }
522
+ getWorkflowStep(runId, name) {
523
+ return this.db.prepare('SELECT status, result FROM workflow_steps WHERE run_id = ? AND name = ?').get(runId, name);
524
+ }
525
+ markStepIntent(runId, name) {
526
+ this.db.prepare(`INSERT INTO workflow_steps (run_id, name, status, intent_ts) VALUES (?, ?, 'intent', ?)
527
+ ON CONFLICT(run_id, name) DO NOTHING`).run(runId, name, new Date().toISOString());
528
+ }
529
+ markStepDone(runId, name, result) {
530
+ this.db.prepare(`UPDATE workflow_steps SET status = 'done', result = ?, done_ts = ? WHERE run_id = ? AND name = ?`).run(JSON.stringify(result), new Date().toISOString(), runId, name);
531
+ }
532
+ settleWorkflowRun(id, deliverable, actor = 'system:async-collect') {
533
+ const run = this.db.transaction(()=>{
534
+ this.db.prepare(`UPDATE workflow_runs SET status = 'settled', deliverable = ?, updated = ? WHERE id = ?`).run(deliverable, new Date().toISOString(), id);
535
+ this.insertEvent({
536
+ actor,
537
+ kind: 'async.settled',
538
+ subjectId: id,
539
+ payload: {
540
+ bytes: deliverable.length
541
+ },
542
+ runId: id
543
+ });
544
+ });
545
+ run();
546
+ }
547
+ requestPendingWrite(input, actor = 'system:writegate') {
548
+ const ts = new Date().toISOString();
549
+ const run = this.db.transaction(()=>{
550
+ const info = this.db.prepare(`INSERT INTO pending_writes (idem_key, seam, payload, status, created, updated)
551
+ VALUES (?, ?, ?, 'pending', ?, ?) ON CONFLICT(idem_key) DO NOTHING`).run(input.idemKey, input.seam, JSON.stringify(input.payload ?? {}), ts, ts);
552
+ if (info.changes === 0) {
553
+ const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ?').get(input.idemKey);
554
+ return {
555
+ created: false,
556
+ status: row.status
557
+ };
558
+ }
559
+ this.insertEvent({
560
+ actor,
561
+ kind: 'write.pending',
562
+ subjectId: input.idemKey,
563
+ payload: {
564
+ seam: input.seam,
565
+ payload: input.payload
566
+ },
567
+ idemKey: `pw:${sha(input.idemKey)}:pending`
568
+ });
569
+ return {
570
+ created: true,
571
+ status: 'pending'
572
+ };
573
+ });
574
+ return run();
575
+ }
576
+ confirmPendingWrite(idemKey, receipt, actor = 'system:writegate') {
577
+ const run = this.db.transaction(()=>{
578
+ const info = this.db.prepare(`UPDATE pending_writes SET status = 'confirmed', receipt = ?, updated = ? WHERE idem_key = ? AND status = 'pending'`).run(receipt, new Date().toISOString(), idemKey);
579
+ if (info.changes === 0) {
580
+ const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ?').get(idemKey);
581
+ return {
582
+ ok: false,
583
+ status: row?.status ?? 'missing'
584
+ };
585
+ }
586
+ this.insertEvent({
587
+ actor,
588
+ kind: 'write.confirmed',
589
+ subjectId: idemKey,
590
+ payload: {
591
+ receipt
592
+ }
593
+ });
594
+ return {
595
+ ok: true,
596
+ status: 'confirmed'
597
+ };
598
+ });
599
+ return run();
600
+ }
601
+ compensatePendingWrite(idemKey, note, actor = 'system:writegate') {
602
+ const run = this.db.transaction(()=>{
603
+ const info = this.db.prepare(`UPDATE pending_writes SET status = 'compensated', receipt = COALESCE(receipt, ?), updated = ? WHERE idem_key = ? AND status != 'compensated'`).run(note, new Date().toISOString(), idemKey);
604
+ if (info.changes === 0) {
605
+ const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ?').get(idemKey);
606
+ return {
607
+ ok: false,
608
+ status: row?.status ?? 'missing'
609
+ };
610
+ }
611
+ this.insertEvent({
612
+ actor,
613
+ kind: 'write.compensated',
614
+ subjectId: idemKey,
615
+ payload: {
616
+ note
617
+ }
618
+ });
619
+ return {
620
+ ok: true,
621
+ status: 'compensated'
622
+ };
623
+ });
624
+ return run();
625
+ }
626
+ listPendingWrites() {
627
+ return this.db.prepare('SELECT idem_key, seam, status, receipt, created FROM pending_writes ORDER BY created').all();
628
+ }
629
+ forkWhatIf(destPath) {
630
+ this.db.prepare('VACUUM INTO ?').run(destPath);
631
+ return destPath;
632
+ }
633
+ forgetSubject(subjects, actor = 'system:state-cli') {
634
+ const run = this.db.transaction(()=>{
635
+ let deleted = 0;
636
+ for (const s of subjects){
637
+ const placeholders = s.kinds.map(()=>'?').join(',');
638
+ const info = this.db.prepare(`DELETE FROM events WHERE subject_id = ? AND kind IN (${placeholders})`).run(s.subjectId, ...s.kinds);
639
+ deleted += info.changes;
640
+ }
641
+ this.insertEvent({
642
+ actor,
643
+ kind: 'forget.executed',
644
+ payload: {
645
+ subjects
646
+ }
647
+ });
648
+ this.rebuildProjections();
649
+ return {
650
+ deleted
651
+ };
652
+ });
653
+ return run();
654
+ }
655
+ close() {
656
+ this.db.close();
657
+ }
658
+ }
659
+ const openLedgers = new Map();
660
+ function openDb(stateRoot) {
661
+ const path = ledgerDbPath(stateRoot);
662
+ const cached = openLedgers.get(path);
663
+ if (cached) return cached;
664
+ mkdirSync(join(path, '..'), {
665
+ recursive: true
666
+ });
667
+ mkdirSync(dirname(path), {
668
+ recursive: true
669
+ });
670
+ const db = new Database(path);
671
+ db.pragma('journal_mode = WAL');
672
+ db.pragma('synchronous = NORMAL');
673
+ db.pragma('busy_timeout = 5000');
674
+ db.exec(SCHEMA);
675
+ db.prepare('INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v').run('schema_version', SCHEMA_VERSION);
676
+ const ledger = new StateLedger(db, stateRoot);
677
+ openLedgers.set(path, ledger);
678
+ return ledger;
679
+ }
680
+ export function openLedgerIfExists(stateRoot) {
681
+ if (!ledgerExists(stateRoot)) return null;
682
+ return openDb(stateRoot);
683
+ }
684
+ export function ensureLedger(stateRoot) {
685
+ if (ledgerExists(stateRoot)) return openDb(stateRoot);
686
+ const dir = stateDirOf(stateRoot);
687
+ const hasLegacy = legacyFilesPresent(stateRoot);
688
+ let backupDir = null;
689
+ if (hasLegacy) {
690
+ backupDir = join(dir, 'pre-ledger-backup');
691
+ mkdirSync(backupDir, {
692
+ recursive: true
693
+ });
694
+ for (const f of LEGACY_FILES){
695
+ if (existsSync(join(dir, f))) copyFileSync(join(dir, f), join(backupDir, f));
696
+ }
697
+ }
698
+ const ledger = openDb(stateRoot);
699
+ if (hasLegacy) importLegacyInto(ledger, dir);
700
+ return ledger;
701
+ }
702
+ function importLegacyInto(ledger, dir) {
703
+ const imported = {
704
+ motivation: false,
705
+ wishes: 0,
706
+ utilityEvents: 0,
707
+ trips: 0,
708
+ companions: 0
709
+ };
710
+ const run = ledger.db.transaction(()=>{
711
+ const flag = ledger.db.prepare(`SELECT v FROM kv WHERE k = 'legacy_imported_v1'`).get();
712
+ if (flag) return;
713
+ const readJsonSafe = (p)=>{
714
+ try {
715
+ return JSON.parse(readFileSync(p, 'utf-8'));
716
+ } catch {
717
+ return null;
718
+ }
719
+ };
720
+ const profile = readJsonSafe(join(dir, 'motivation-profile.json'));
721
+ if (profile) {
722
+ if (ledger.insertEvent({
723
+ actor: 'system:migrate',
724
+ kind: 'motivation.imported',
725
+ subjectId: 'motivation',
726
+ payload: {
727
+ profile
728
+ },
729
+ idemKey: 'import:motivation'
730
+ }) !== null) imported.motivation = true;
731
+ }
732
+ const pool = readJsonSafe(join(dir, 'wish-pool.json'));
733
+ if (Array.isArray(pool)) {
734
+ for (const w of pool){
735
+ const id = String(w['wish_id'] ?? makeWishId(String(w['name'] ?? '')));
736
+ const wish = {
737
+ ...w,
738
+ wish_id: id
739
+ };
740
+ if (ledger.insertEvent({
741
+ actor: 'system:migrate',
742
+ kind: 'wish.imported',
743
+ subjectId: id,
744
+ payload: {
745
+ wish
746
+ },
747
+ idemKey: `import:wish:${id}`
748
+ }) !== null) imported.wishes++;
749
+ }
750
+ }
751
+ try {
752
+ const lines = readFileSync(join(dir, 'memory-utility.jsonl'), 'utf-8').split('\n').filter(Boolean);
753
+ for (const line of lines){
754
+ try {
755
+ const ev = JSON.parse(line);
756
+ if (ledger.insertEvent({
757
+ actor: 'system:migrate',
758
+ kind: 'memory_utility.event',
759
+ subjectId: ev.wish_id,
760
+ payload: {
761
+ event: ev
762
+ },
763
+ idemKey: `mu:${ev.event_id}`
764
+ }) !== null) imported.utilityEvents++;
765
+ } catch {}
766
+ }
767
+ } catch {}
768
+ try {
769
+ const lines = readFileSync(join(dir, 'trips.jsonl'), 'utf-8').split('\n').filter(Boolean);
770
+ for (const line of lines){
771
+ try {
772
+ const trip = JSON.parse(line);
773
+ if (ledger.insertEvent({
774
+ actor: 'system:migrate',
775
+ kind: 'trip.logged',
776
+ subjectId: trip.trip_id,
777
+ payload: {
778
+ trip
779
+ },
780
+ idemKey: `trip:${trip.trip_id}`
781
+ }) !== null) imported.trips++;
782
+ } catch {}
783
+ }
784
+ } catch {}
785
+ const companions = readJsonSafe(join(dir, 'companions.json'));
786
+ if (Array.isArray(companions)) {
787
+ for (const c of companions){
788
+ const id = String(c['companion_id'] ?? '');
789
+ if (!id) continue;
790
+ if (ledger.insertEvent({
791
+ actor: 'system:migrate',
792
+ kind: 'companion.imported',
793
+ subjectId: id,
794
+ payload: {
795
+ profile: c
796
+ },
797
+ idemKey: `import:companion:${id}`
798
+ }) !== null) imported.companions++;
799
+ }
800
+ }
801
+ ledger.rebuildProjections();
802
+ ledger.db.prepare(`INSERT INTO kv (k, v) VALUES ('legacy_imported_v1', 'done') ON CONFLICT(k) DO UPDATE SET v = excluded.v`).run();
803
+ });
804
+ run();
805
+ return imported;
806
+ }
807
+ function readLegacyJson(stateRoot, name, fallback) {
808
+ try {
809
+ return JSON.parse(readFileSync(join(stateDirOf(stateRoot), name), 'utf-8'));
810
+ } catch {
811
+ return fallback;
812
+ }
813
+ }
814
+ function readLegacyJsonl(stateRoot, name) {
815
+ try {
816
+ return readFileSync(join(stateDirOf(stateRoot), name), 'utf-8').split('\n').filter(Boolean).map((l)=>JSON.parse(l));
817
+ } catch {
818
+ return [];
819
+ }
820
+ }
821
+ export function readMotivationWithFallback(stateRoot) {
822
+ const ledger = openLedgerIfExists(stateRoot);
823
+ if (ledger) return ledger.readMotivation();
824
+ return readLegacyJson(stateRoot, 'motivation-profile.json', null);
825
+ }
826
+ export function readWishPoolWithFallback(stateRoot) {
827
+ const ledger = openLedgerIfExists(stateRoot);
828
+ if (ledger) return ledger.readWishPool();
829
+ return readLegacyJson(stateRoot, 'wish-pool.json', []);
830
+ }
831
+ export function readUtilityEventsWithFallback(stateRoot) {
832
+ const ledger = openLedgerIfExists(stateRoot);
833
+ if (ledger) return ledger.readUtilityEvents();
834
+ return readLegacyJsonl(stateRoot, 'memory-utility.jsonl');
835
+ }
836
+ export function readTripsWithFallback(stateRoot) {
837
+ const ledger = openLedgerIfExists(stateRoot);
838
+ if (ledger) return ledger.readTrips();
839
+ return readLegacyJsonl(stateRoot, 'trips.jsonl');
840
+ }
841
+ export function readCompanionsWithFallback(stateRoot) {
842
+ const ledger = openLedgerIfExists(stateRoot);
843
+ if (ledger) return ledger.readCompanions();
844
+ return readLegacyJson(stateRoot, 'companions.json', []);
845
+ }
846
+
847
+
848
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/state-ledger.ts