@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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -0
  3. package/bin/gotry-inner.js +214 -0
  4. package/bin/gotry-stdio-ask.js +59 -0
  5. package/bin/gotry.js +28 -0
  6. package/cordis.gotry-patch.yml +86 -0
  7. package/data/flights_2026.json +218 -0
  8. package/data/golden_erhai.json +92 -0
  9. package/data/golden_trip_2026.json +75 -0
  10. package/data/hotels_2026.json +151 -0
  11. package/data/openflights-skeleton.json +895 -0
  12. package/data/time-slot-eval.json +384 -0
  13. package/data/yunnan-pack.json +202 -0
  14. package/dist/capabilities/agent-reach-bridge.py +90 -0
  15. package/dist/capabilities/agent-reach-deep.js +172 -0
  16. package/dist/capabilities/agent-reach.js +204 -0
  17. package/dist/capabilities/anything.js +139 -0
  18. package/dist/capabilities/flyai.js +131 -0
  19. package/dist/capabilities/hbcli.js +143 -0
  20. package/dist/capabilities/incident-log.js +100 -0
  21. package/dist/capabilities/opensky.js +74 -0
  22. package/dist/capabilities/session/action-cache.js +120 -0
  23. package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
  24. package/dist/capabilities/session/adapters/meituan-local.js +73 -0
  25. package/dist/capabilities/session/extract.js +40 -0
  26. package/dist/capabilities/session/read-guard.js +99 -0
  27. package/dist/capabilities/session/transport.js +101 -0
  28. package/dist/capabilities/session-search.js +93 -0
  29. package/dist/capabilities/weather.js +180 -0
  30. package/dist/scripts/action-cache-tests.js +107 -0
  31. package/dist/scripts/agent-reach-deep-tests.js +54 -0
  32. package/dist/scripts/agent-reach-tests.js +38 -0
  33. package/dist/scripts/agent-reach-wrapper-tests.js +79 -0
  34. package/dist/scripts/anything-tests.js +95 -0
  35. package/dist/scripts/async-collect.js +37 -0
  36. package/dist/scripts/companion-tests.js +112 -0
  37. package/dist/scripts/diff-test.js +39 -0
  38. package/dist/scripts/engine-run.js +14 -0
  39. package/dist/scripts/engine-tests.js +45 -0
  40. package/dist/scripts/hbcli-tests.js +101 -0
  41. package/dist/scripts/incident-tests.js +96 -0
  42. package/dist/scripts/journey-tests.js +45 -0
  43. package/dist/scripts/ledger-tests.js +365 -0
  44. package/dist/scripts/ledger-workflow-crash.js +42 -0
  45. package/dist/scripts/memory-capture-tests.js +77 -0
  46. package/dist/scripts/memory-decay-tests.js +83 -0
  47. package/dist/scripts/memory-metrics.js +24 -0
  48. package/dist/scripts/nudge-digest.js +83 -0
  49. package/dist/scripts/opensky-check.js +20 -0
  50. package/dist/scripts/opensky-tests.js +52 -0
  51. package/dist/scripts/probe-poi-tests.js +88 -0
  52. package/dist/scripts/publish-preverify.js +68 -0
  53. package/dist/scripts/replay-async.js +44 -0
  54. package/dist/scripts/replay-real.js +38 -0
  55. package/dist/scripts/replay.js +116 -0
  56. package/dist/scripts/session-attach-diagnose.js +27 -0
  57. package/dist/scripts/session-attach-poc.js +65 -0
  58. package/dist/scripts/session-extract-tests.js +81 -0
  59. package/dist/scripts/session-tests.js +162 -0
  60. package/dist/scripts/skeleton-check.js +37 -0
  61. package/dist/scripts/skeleton-integration-test.js +22 -0
  62. package/dist/scripts/skills-contract-tests.js +85 -0
  63. package/dist/scripts/smoke.js +328 -0
  64. package/dist/scripts/state-cli-tests.js +136 -0
  65. package/dist/scripts/state-cli.js +248 -0
  66. package/dist/scripts/time-eval-tests.js +318 -0
  67. package/dist/scripts/travel-timeline-tests.js +123 -0
  68. package/dist/scripts/unified-tests.js +49 -0
  69. package/dist/scripts/weather-tests.js +48 -0
  70. package/dist/src/bridge.js +34 -0
  71. package/dist/src/companions.js +112 -0
  72. package/dist/src/contracts.js +64 -0
  73. package/dist/src/dsh-llm.js +174 -0
  74. package/dist/src/engine.js +331 -0
  75. package/dist/src/index.js +1239 -0
  76. package/dist/src/journey.js +147 -0
  77. package/dist/src/loop.js +360 -0
  78. package/dist/src/memory-capture.js +40 -0
  79. package/dist/src/memory-decay.js +31 -0
  80. package/dist/src/memory-utility.js +55 -0
  81. package/dist/src/mock-llm.js +103 -0
  82. package/dist/src/model.js +134 -0
  83. package/dist/src/slot-spec.js +165 -0
  84. package/dist/src/state-ledger.js +901 -0
  85. package/dist/src/time-anchor.js +100 -0
  86. package/dist/src/tool-packet.js +12 -0
  87. package/dist/src/travel-slots.js +144 -0
  88. package/dist/src/travel-timeline.js +78 -0
  89. package/dist/src/unified.js +538 -0
  90. package/dist/src/wish-pool.js +27 -0
  91. package/package.json +84 -0
  92. package/ts/capabilities/agent-reach-deep.ts +114 -0
  93. package/ts/capabilities/agent-reach.ts +158 -0
  94. package/ts/capabilities/anything.ts +201 -0
  95. package/ts/capabilities/flyai.ts +151 -0
  96. package/ts/capabilities/hbcli.ts +152 -0
  97. package/ts/capabilities/incident-log.ts +148 -0
  98. package/ts/capabilities/opensky.ts +121 -0
  99. package/ts/capabilities/session/action-cache.ts +148 -0
  100. package/ts/capabilities/session/adapters/ctrip-flight.ts +107 -0
  101. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  102. package/ts/capabilities/session/extract.ts +54 -0
  103. package/ts/capabilities/session/read-guard.ts +141 -0
  104. package/ts/capabilities/session/transport.ts +114 -0
  105. package/ts/capabilities/session-search.ts +129 -0
  106. package/ts/capabilities/weather.ts +176 -0
  107. package/ts/package.json +27 -0
  108. package/ts/scripts/async-collect.ts +50 -0
  109. package/ts/scripts/skeleton-check.ts +49 -0
  110. package/ts/scripts/skeleton-integration-test.ts +23 -0
  111. package/ts/scripts/state-cli.ts +201 -0
  112. package/ts/src/bridge.ts +44 -0
  113. package/ts/src/companions.ts +113 -0
  114. package/ts/src/contracts.ts +165 -0
  115. package/ts/src/dsh-llm.ts +155 -0
  116. package/ts/src/index.ts +968 -0
  117. package/ts/src/loop.ts +411 -0
  118. package/ts/src/memory-capture.ts +59 -0
  119. package/ts/src/memory-decay.ts +55 -0
  120. package/ts/src/memory-utility.ts +70 -0
  121. package/ts/src/mock-llm.ts +100 -0
  122. package/ts/src/model.ts +220 -0
  123. package/ts/src/slot-spec.ts +172 -0
  124. package/ts/src/state-ledger.ts +849 -0
  125. package/ts/src/time-anchor.ts +134 -0
  126. package/ts/src/tool-packet.ts +32 -0
  127. package/ts/src/travel-slots.ts +219 -0
  128. package/ts/src/travel-timeline.ts +83 -0
  129. package/ts/src/unified.ts +582 -0
  130. 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