@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,344 @@
1
+ import Database from 'better-sqlite3';
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { ensureLedger, openLedgerIfExists } from '../src/state-ledger.js';
7
+ let pass = 0;
8
+ let fail = 0;
9
+ function assert(cond, msg) {
10
+ if (cond) {
11
+ pass++;
12
+ console.log(` ok - ${msg}`);
13
+ } else {
14
+ fail++;
15
+ console.error(` FAIL - ${msg}`);
16
+ }
17
+ }
18
+ const root = mkdtempSync(join(tmpdir(), 'gotry-ledger-'));
19
+ const ledger = ensureLedger(root);
20
+ const r1 = ledger.appendMotivationPatch({
21
+ weights: {
22
+ escape_rest: 0.7
23
+ },
24
+ evidence: [
25
+ '用户原话:想去湖边什么都不干'
26
+ ],
27
+ hard: {
28
+ wake_not_before: '06:30'
29
+ }
30
+ });
31
+ assert(r1.saved === true, '画像补丁落账(saved)');
32
+ const r2 = ledger.appendMotivationPatch({
33
+ evidence: [
34
+ '用户原话:想去湖边什么都不干'
35
+ ]
36
+ });
37
+ assert(r2.saved === false, '同 evidence 重放幂等(mergeProfile 守门 → 零事件)');
38
+ const before = ledger.countEvents();
39
+ let threw = false;
40
+ try {
41
+ ledger.db.transaction(()=>{
42
+ ledger.insertEvent({
43
+ actor: 'test',
44
+ kind: 'probe',
45
+ payload: {
46
+ x: 1
47
+ }
48
+ });
49
+ throw new Error('boom');
50
+ })();
51
+ } catch {
52
+ threw = true;
53
+ }
54
+ assert(threw && ledger.countEvents() === before, '事务内异常 → 整体回滚,账本无痕(崩溃一致性的单事务证明)');
55
+ threw = false;
56
+ try {
57
+ ledger.appendWish({
58
+ name: '无条件憧憬',
59
+ reason: '',
60
+ conditions: undefined
61
+ });
62
+ } catch {
63
+ threw = true;
64
+ }
65
+ assert(threw && ledger.countEvents() === before, 'conditions 红线在事务内拒绝且零事件(红线进 schema 层)');
66
+ const w1 = ledger.appendWish({
67
+ name: '大理·洱海',
68
+ reason: '',
69
+ conditions: {
70
+ days: 5,
71
+ budget_cny: 4950,
72
+ best_months: [
73
+ 3,
74
+ 4
75
+ ]
76
+ }
77
+ });
78
+ const w2 = ledger.appendWish({
79
+ name: '大理·洱海',
80
+ reason: '两周年想再去',
81
+ conditions: {
82
+ days: 6
83
+ }
84
+ });
85
+ assert(w1.added === true && w2.added === false && w1.wish_id === w2.wish_id, '同名愿望幂等更新,wish_id 语义派生稳定');
86
+ assert(ledger.readWishPool().length === 1, '愿望池投影恰 1 条');
87
+ const u1 = ledger.appendUtilityEvent({
88
+ wish_id: w1.wish_id,
89
+ kind: 'recalled',
90
+ ts: '2026-08-28T00:00:00Z',
91
+ ctx: 'test'
92
+ });
93
+ const u2 = ledger.appendUtilityEvent({
94
+ wish_id: w1.wish_id,
95
+ kind: 'recalled',
96
+ ts: '2026-08-28T00:00:00Z',
97
+ ctx: 'test'
98
+ });
99
+ assert(u1.appended === true && u2.appended === false, '效用事件语义键幂等(重放 no-op)');
100
+ ledger.appendUtilityEvent({
101
+ wish_id: w1.wish_id,
102
+ kind: 'verified_outcome',
103
+ ts: '2026-08-28T00:00:00Z',
104
+ ctx: 'test-noattr'
105
+ });
106
+ assert(ledger.readUtilityEvents().at(-1)?.kind === 'applied', '无归因 verified 降级 applied(六件语义纪律随账本)');
107
+ const t1 = ledger.appendTripEvent({
108
+ destination: '大理',
109
+ start: '2026-10-01',
110
+ source: 'user-verbatim',
111
+ evidence: '用户原话:国庆去了大理'
112
+ });
113
+ const t1dup = ledger.appendTripEvent({
114
+ destination: '大理',
115
+ start: '2026-10-01',
116
+ source: 'user-verbatim',
117
+ evidence: '用户原话:国庆去了大理'
118
+ });
119
+ assert(t1.appended === true && t1dup.appended === false, '行程 trip_id 幂等');
120
+ const tOverlap = ledger.appendTripEvent({
121
+ destination: '大理',
122
+ start: '2026-10-03',
123
+ source: 'user-verbatim',
124
+ evidence: '重叠探针'
125
+ });
126
+ assert(tOverlap.appended === false && /重叠/.test(tOverlap.reason ?? ''), '同目的地日期重叠冲突即停(由人裁决)');
127
+ const tBad = ledger.appendTripEvent({
128
+ destination: 'X地',
129
+ start: '十月一',
130
+ source: 'user-verbatim',
131
+ evidence: 'x'
132
+ });
133
+ assert(tBad.appended === false, '词表外日期拒收(不猜)');
134
+ const cBad = ledger.appendCompanion({
135
+ label: '爸爸',
136
+ constraints: {
137
+ health: [
138
+ '手机号13800001111'
139
+ ]
140
+ },
141
+ evidence: '探针'
142
+ });
143
+ assert(cBad.appended === false && /负面清单/.test(cBad.reason ?? ''), '同行人负面清单拒收(证件/手机号不入库)');
144
+ const cOk = ledger.appendCompanion({
145
+ label: '爸爸',
146
+ constraints: {
147
+ health: [
148
+ '轻度高血压'
149
+ ]
150
+ },
151
+ evidence: '用户原话:爸爸65轻度高血压'
152
+ });
153
+ assert(cOk.appended === true && ledger.readCompanions().length === 1, '同行人正常入账');
154
+ const co = ledger.confirmOutcome({
155
+ wishId: w1.wish_id,
156
+ attribution: 'helpful',
157
+ detail: '成了',
158
+ trip: {
159
+ destination: '大理',
160
+ start: '2026-10-01',
161
+ source: 'wish-confirmed',
162
+ evidence: `wish ${w1.wish_id} confirm-outcome(helpful)`
163
+ }
164
+ });
165
+ assert(co.recorded === true, 'confirm-outcome:verified_outcome 效用事件落账');
166
+ assert(co.trip?.appended === false && /重叠/.test(co.trip?.reason ?? ''), 'confirm-outcome 单事务:行程过守门(重叠拒收,与文件版语义一致)——两写同生或同拒');
167
+ const poolBefore = JSON.stringify(ledger.readWishPool());
168
+ const profileBefore = ledger.readMotivation();
169
+ ledger.db.exec('DELETE FROM projection_docs; DELETE FROM projection_items');
170
+ ledger.rebuildProjections();
171
+ assert(JSON.stringify(ledger.readWishPool()) === poolBefore, 'fold 重建:愿望池与直读逐字节一致');
172
+ assert(JSON.stringify(ledger.readMotivation()?.weights) === JSON.stringify(profileBefore?.weights) && (ledger.readMotivation()?.evidence?.length ?? 0) === (profileBefore?.evidence?.length ?? 0), 'fold 重建:画像与直读一致');
173
+ assert(ledger.readCompanions().length === 1 && ledger.readTrips().length === 1, 'fold 重建:同行人投影与行程日志一致');
174
+ const wishAddedSeqs = ledger.readEvents('wish.added', 10).map((e)=>e.seq);
175
+ const firstWishSeq = Math.min(...wishAddedSeqs);
176
+ ledger.appendWish({
177
+ name: '普吉',
178
+ reason: '',
179
+ conditions: {
180
+ days: 5
181
+ }
182
+ });
183
+ ledger.rebuildProjections(firstWishSeq);
184
+ assert(ledger.readWishPool().length === 1, `rewind 至 seq ${firstWishSeq}:投影回到历史时点(LangGraph fork 同构)`);
185
+ ledger.rebuildProjections();
186
+ assert(ledger.readWishPool().length === 2, 'rebuild 无参回到最新(events 是唯一权威,投影随时可重建)');
187
+ const mroot = mkdtempSync(join(tmpdir(), 'gotry-migrate-'));
188
+ const mdir = join(mroot, 'gotry-state');
189
+ mkdirSync(mdir, {
190
+ recursive: true
191
+ });
192
+ writeFileSync(join(mdir, 'motivation-profile.json'), JSON.stringify({
193
+ weights: {
194
+ curiosity: 0.6
195
+ },
196
+ evidence: [
197
+ '原话A'
198
+ ],
199
+ hard: {},
200
+ updated_at: '2026-08-01T00:00:00Z'
201
+ }));
202
+ writeFileSync(join(mdir, 'wish-pool.json'), JSON.stringify([
203
+ {
204
+ wish_id: 'wLEGACY1',
205
+ name: '京都',
206
+ conditions: {
207
+ days: 7
208
+ },
209
+ added_at: '2026-08-01T00:00:00Z'
210
+ }
211
+ ]));
212
+ writeFileSync(join(mdir, 'memory-utility.jsonl'), JSON.stringify({
213
+ schema: 'memory_utility_observation.v0',
214
+ event_id: 'wLEGACY1|recalled||',
215
+ wish_id: 'wLEGACY1',
216
+ kind: 'recalled',
217
+ ts: '2026-08-02T00:00:00Z'
218
+ }) + '\n');
219
+ writeFileSync(join(mdir, 'trips.jsonl'), JSON.stringify({
220
+ schema: 'travel_timeline.v1',
221
+ trip_id: '京都|2026-04-01|user-verbatim',
222
+ destination: '京都',
223
+ start: '2026-04-01',
224
+ source: 'user-verbatim',
225
+ evidence: '原话',
226
+ ts: '2026-08-01T00:00:00Z'
227
+ }) + '\n');
228
+ writeFileSync(join(mdir, 'companions.json'), JSON.stringify([
229
+ {
230
+ schema: 'companion_profile.v1',
231
+ companion_id: '妈妈',
232
+ label: '妈妈',
233
+ constraints: {
234
+ mobility: '步行≤3h'
235
+ },
236
+ evidence: [
237
+ '原话'
238
+ ],
239
+ ts: '2026-08-01T00:00:00Z'
240
+ }
241
+ ]));
242
+ const m1 = ensureLedger(mroot);
243
+ assert(m1.readMotivation()?.weights?.['curiosity'] === 0.6, '迁移:画像整档入账');
244
+ assert(String(m1.readWishPool()[0]?.wish_id) === 'wLEGACY1', '迁移:愿望保留原主键');
245
+ assert(m1.readUtilityEvents().length === 1 && m1.readTrips().length === 1 && m1.readCompanions().length === 1, '迁移:效用/行程/同行人入账');
246
+ assert(existsSync(join(mdir, 'pre-ledger-backup', 'wish-pool.json')), '迁移:导入前快照存在(pre-ledger-backup/)');
247
+ const migratedCount = m1.countEvents();
248
+ ensureLedger(mroot);
249
+ assert(m1.countEvents() === migratedCount, '重复 ensure 不重复导入(kv 旗标 + 幂等键双保险)');
250
+ const wroot = mkdtempSync(join(tmpdir(), 'gotry-wf-'));
251
+ const countFile = join(wroot, 'solve-count.txt');
252
+ writeFileSync(countFile, '');
253
+ const crash = spawnSync('npx', [
254
+ 'tsx',
255
+ 'scripts/ledger-workflow-crash.ts',
256
+ wroot,
257
+ 'dp-crash1',
258
+ countFile
259
+ ], {
260
+ encoding: 'utf-8'
261
+ });
262
+ assert(crash.status === 9, `崩溃探针按设计 exit 9(实际 ${crash.status}:${(crash.stderr ?? '').slice(0, 200)})`);
263
+ const runRow = openLedgerIfExists(wroot)?.getWorkflowRun('dp-crash1');
264
+ assert(runRow?.status === 'pending', '崩溃后工单仍 pending(账本权威未损)');
265
+ const count1 = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
266
+ assert(count1 === 1, '崩溃前真实求解恰 1 次');
267
+ const resume = spawnSync('npx', [
268
+ 'tsx',
269
+ 'scripts/async-collect.ts',
270
+ 'dp-crash1',
271
+ wroot
272
+ ], {
273
+ encoding: 'utf-8',
274
+ env: {
275
+ ...process.env,
276
+ GOTRY_SOLVE_COUNT_FILE: countFile
277
+ }
278
+ });
279
+ assert(resume.status === 0, `恢复进程 exit 0(实际 ${resume.status}:${(resume.stderr ?? '').slice(0, 300)})`);
280
+ const count2 = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
281
+ assert(count2 === 1, '恢复时 done 步骤零重算(exactly-once:求解计数仍 1,不重复花钱)');
282
+ assert(existsSync(join(wroot, 'gotry-state', 'async', 'dp-crash1.deliverable.md')), '交付物视图已落盘(.deliverable.md)');
283
+ assert(openLedgerIfExists(wroot)?.getWorkflowRun('dp-crash1')?.status === 'settled', '账本终态 settled');
284
+ const replay = spawnSync('npx', [
285
+ 'tsx',
286
+ 'scripts/async-collect.ts',
287
+ 'dp-crash1',
288
+ wroot
289
+ ], {
290
+ encoding: 'utf-8',
291
+ env: {
292
+ ...process.env,
293
+ GOTRY_SOLVE_COUNT_FILE: countFile
294
+ }
295
+ });
296
+ assert(replay.status === 0, '终态复诵 exit 0(幂等)');
297
+ const count3 = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
298
+ assert(count3 === 1, '终态复诵零重算');
299
+ const pw1 = ledger.requestPendingWrite({
300
+ idemKey: 'booking:demo-1',
301
+ seam: 'flight-order-confirm',
302
+ payload: {
303
+ flight: 'MU123'
304
+ }
305
+ });
306
+ const pw2 = ledger.requestPendingWrite({
307
+ idemKey: 'booking:demo-1',
308
+ seam: 'flight-order-confirm',
309
+ payload: {
310
+ flight: 'MU123'
311
+ }
312
+ });
313
+ assert(pw1.created === true && pw2.created === false, 'pending_writes 幂等键去重(同一确认不可能登记两次)');
314
+ const cf1 = ledger.confirmPendingWrite('booking:demo-1', 'PNR-ABC');
315
+ const cf2 = ledger.confirmPendingWrite('booking:demo-1', 'PNR-ABC2');
316
+ assert(cf1.ok === true && cf2.ok === false && cf2.status === 'confirmed', 'L3 确认只能发生一次,二连击被拒');
317
+ const cp1 = ledger.compensatePendingWrite('booking:demo-1', '用户改签退款');
318
+ assert(cp1.ok === true && ledger.listPendingWrites()[0]?.status === 'compensated', 'saga 补偿可达(pending/confirmed → compensated)');
319
+ const audit = ledger.readEvents(undefined, 100).filter((e)=>e.kind.startsWith('write.'));
320
+ assert(audit.some((e)=>e.kind === 'write.pending') && audit.some((e)=>e.kind === 'write.confirmed') && audit.some((e)=>e.kind === 'write.compensated'), '写权审计事件链完整(append-only,WriteGate 的 receipt 落点)');
321
+ const forkPath = join(root, 'whatif.db');
322
+ ledger.forkWhatIf(forkPath);
323
+ const forkDb = new Database(forkPath);
324
+ forkDb.prepare("INSERT INTO events (ts, actor, kind, subject_id, payload) VALUES (?, 'test', 'probe', '', '{}')").run(new Date().toISOString());
325
+ const forkCount = forkDb.prepare('SELECT COUNT(*) AS n FROM events').get().n;
326
+ forkDb.close();
327
+ assert(ledger.countEvents() === forkCount - 1, 'what-if 分叉:副本写入不触正本(VACUUM INTO 预演)');
328
+ rmSync(root, {
329
+ recursive: true,
330
+ force: true
331
+ });
332
+ rmSync(mroot, {
333
+ recursive: true,
334
+ force: true
335
+ });
336
+ rmSync(wroot, {
337
+ recursive: true,
338
+ force: true
339
+ });
340
+ console.log(`\nLEDGER TESTS: ${pass} ok, ${fail} fail`);
341
+ if (fail > 0) process.exit(1);
342
+
343
+
344
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/ledger-tests.ts
@@ -0,0 +1,42 @@
1
+ import { appendFileSync } from 'node:fs';
2
+ import { collectDeepPlanning, makeJournaledSolvePort, persistAsyncTicket } from '../src/loop.js';
3
+ import { openLedgerIfExists } from '../src/state-ledger.js';
4
+ const [root, id, countFile] = process.argv.slice(2);
5
+ if (!root || !id || !countFile) {
6
+ console.error('用法:npx tsx scripts/ledger-workflow-crash.ts <stateRoot> <ticketId> <countFile>');
7
+ process.exit(1);
8
+ }
9
+ const ticket = {
10
+ id,
11
+ objective: '崩溃注入:已求解未交付(settle 前被杀)',
12
+ requestedAt: new Date().toISOString(),
13
+ etaLabel: '秒级'
14
+ };
15
+ const state = {
16
+ calendar: {
17
+ year: 2026,
18
+ assertedWeekdays: {}
19
+ },
20
+ profile: {},
21
+ gates: [],
22
+ wishes: [],
23
+ spec: {
24
+ segments: []
25
+ }
26
+ };
27
+ await persistAsyncTicket(ticket, state, root);
28
+ const ledger = openLedgerIfExists(root);
29
+ const solve = makeJournaledSolvePort(ledger, id, async ()=>({
30
+ feasible: false,
31
+ unsat_core: [
32
+ 'crash-probe'
33
+ ],
34
+ suggestions: []
35
+ }), {
36
+ onRealSolve: ()=>appendFileSync(countFile, 'solve\n')
37
+ });
38
+ await collectDeepPlanning(state, ticket, solve);
39
+ process.exit(9);
40
+
41
+
42
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/ledger-workflow-crash.ts
@@ -0,0 +1,83 @@
1
+ import assert from 'node:assert/strict';
2
+ import { decayedConfidence, eventDecayScore, windowFactor } from '../src/memory-decay.js';
3
+ const NOW = new Date(2026, 7, 28, 12);
4
+ let n = 0;
5
+ function pass(name, body) {
6
+ body();
7
+ console.log(` ${++n}. ${name} OK`);
8
+ }
9
+ pass('分级窗口:边界值与地板', ()=>{
10
+ assert.equal(windowFactor(0), 1.0);
11
+ assert.equal(windowFactor(30), 1.0);
12
+ assert.equal(windowFactor(31), 0.75);
13
+ assert.equal(windowFactor(90), 0.75);
14
+ assert.equal(windowFactor(91), 0.5);
15
+ assert.equal(windowFactor(180), 0.5);
16
+ assert.equal(windowFactor(181), 0.25);
17
+ assert.equal(windowFactor(365), 0.25);
18
+ assert.equal(windowFactor(366), 0.1);
19
+ assert.equal(windowFactor(3000), 0.1, '地板 0.1:旧而不灭');
20
+ });
21
+ pass('种类权重:verified > applied > recalled(自称被召回 ≠ 有用)', ()=>{
22
+ const ts = '2026-08-27T00:00:00Z';
23
+ assert.ok(eventDecayScore({
24
+ ts,
25
+ kind: 'verified_outcome'
26
+ }, NOW) > eventDecayScore({
27
+ ts,
28
+ kind: 'applied'
29
+ }, NOW));
30
+ assert.ok(eventDecayScore({
31
+ ts,
32
+ kind: 'applied'
33
+ }, NOW) > eventDecayScore({
34
+ ts,
35
+ kind: 'recalled'
36
+ }, NOW));
37
+ });
38
+ pass('单调:同种类,新事件分 ≥ 旧事件分', ()=>{
39
+ const fresh = eventDecayScore({
40
+ ts: '2026-08-27T00:00:00Z',
41
+ kind: 'recalled'
42
+ }, NOW);
43
+ const old = eventDecayScore({
44
+ ts: '2025-01-01T00:00:00Z',
45
+ kind: 'recalled'
46
+ }, NOW);
47
+ assert.ok(fresh >= old);
48
+ assert.equal(old, 0.025, '地板乘子=0.25×0.1(一年外)');
49
+ });
50
+ pass('置信度上界 1 且多事件累加有界', ()=>{
51
+ const many = Array.from({
52
+ length: 50
53
+ }, ()=>({
54
+ ts: '2026-08-27T00:00:00Z',
55
+ kind: 'verified_outcome'
56
+ }));
57
+ assert.equal(decayedConfidence(many, NOW), 1);
58
+ const one = decayedConfidence([
59
+ {
60
+ ts: '2026-08-27T00:00:00Z',
61
+ kind: 'verified_outcome'
62
+ }
63
+ ], NOW);
64
+ assert.ok(one > 0 && one <= 1);
65
+ });
66
+ pass('动机层零衰减(构造性):本模块无作用于 motivation-profile 的 API,权重原样透传', ()=>{
67
+ const weights = {
68
+ escape_rest: 0.4,
69
+ curiosity: 0.09
70
+ };
71
+ const snapshot = JSON.stringify(weights);
72
+ void decayedConfidence([
73
+ {
74
+ ts: '2020-01-01T00:00:00Z',
75
+ kind: 'recalled'
76
+ }
77
+ ], NOW);
78
+ assert.equal(JSON.stringify(weights), snapshot);
79
+ });
80
+ console.log(`\nMEMORY DECAY TESTS: ${n}/5 OK(memory-design P3 分级窗口/单调/地板/上界/动机零衰减)`);
81
+
82
+
83
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/memory-decay-tests.ts
@@ -1,24 +1,9 @@
1
- import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
1
  import { projectUtility } from '../src/memory-utility.js';
2
+ import { decayedConfidence } from '../src/memory-decay.js';
3
+ import { readUtilityEventsWithFallback, readWishPoolWithFallback } from '../src/state-ledger.js';
4
4
  const stateRoot = process.argv[2] ?? '.';
5
- const stateDir = join(stateRoot, 'gotry-state');
6
- function readJson(path, fallback) {
7
- try {
8
- return JSON.parse(readFileSync(path, 'utf-8'));
9
- } catch {
10
- return fallback;
11
- }
12
- }
13
- function readJsonl(path) {
14
- try {
15
- return readFileSync(path, 'utf-8').split('\n').filter(Boolean).map((l)=>JSON.parse(l));
16
- } catch {
17
- return [];
18
- }
19
- }
20
- const pool = readJson(join(stateDir, 'wish-pool.json'), []);
21
- const events = readJsonl(join(stateDir, 'memory-utility.jsonl'));
5
+ const pool = readWishPoolWithFallback(stateRoot);
6
+ const events = readUtilityEventsWithFallback(stateRoot);
22
7
  const projection = projectUtility(events);
23
8
  const active = pool.filter((w)=>typeof w.wish_id === 'string');
24
9
  const recalledWishes = Object.values(projection).filter((w)=>w.recalled > 0).length;
@@ -29,7 +14,8 @@ console.log(`wish pool: ${pool.length} 条在册(其中 ${pool.filter((w)=>w.mut
29
14
  console.log(`效用事件: ${events.length} 条(recalled=${events.filter((e)=>e.kind === 'recalled').length}, applied=${events.filter((e)=>e.kind === 'applied').length}, verified=${events.filter((e)=>e.kind === 'verified_outcome').length})`);
30
15
  for (const w of Object.values(projection)){
31
16
  const name = pool.find((p)=>p.wish_id === w.wish_id)?.name ?? w.wish_id;
32
- console.log(` - ${name}: status=${w.status}, recalled=${w.recalled}, applied=${w.applied}, verified=${w.verified}`);
17
+ const conf = decayedConfidence(events.filter((e)=>e.wish_id === w.wish_id), new Date());
18
+ console.log(` - ${name}: status=${w.status}, recalled=${w.recalled}, applied=${w.applied}, verified=${w.verified}, 新鲜置信度=${conf}(P3 时间窗衰减)`);
33
19
  }
34
20
  console.log(`经验回流率基线 = ${verifiedWishes}/${recalledWishes} = ${refluxBaseline.toFixed(2)}(verified/recalled;单用户起步期样本稀疏属预期)`);
35
21
  if (pool.length === 0) console.log('(wish pool 为空——首访用户,指标从首条憧憬入池开始积累)');
@@ -1,33 +1,23 @@
1
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
1
+ import { writeFileSync, mkdirSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
3
  import { pickNudgeWish } from '../src/wish-pool.js';
4
4
  import { projectUtility } from '../src/memory-utility.js';
5
5
  import { buildTimeAnchor } from '../src/time-anchor.js';
6
+ import { readUtilityEventsWithFallback, readWishPoolWithFallback } from '../src/state-ledger.js';
6
7
  function arg(name) {
7
8
  const i = process.argv.indexOf(name);
8
9
  return i >= 0 ? process.argv[i + 1] : undefined;
9
10
  }
10
11
  const stateRoot = arg('--state-root') ?? '.';
11
12
  const stateDir = join(stateRoot, 'gotry-state');
12
- function readJson(path, fallback) {
13
- try {
14
- return JSON.parse(readFileSync(path, 'utf-8'));
15
- } catch {
16
- return fallback;
17
- }
18
- }
19
13
  function readEvents() {
20
- try {
21
- return readFileSync(join(stateDir, 'memory-utility.jsonl'), 'utf-8').split('\n').filter(Boolean).map((l)=>JSON.parse(l));
22
- } catch {
23
- return [];
24
- }
14
+ return readUtilityEventsWithFallback(stateRoot);
25
15
  }
26
16
  if (process.env['GOTRY_NUDGE_ENABLED'] === 'false') {
27
17
  console.log('回访已关闭(GOTRY_NUDGE_ENABLED=false)——可关闭契约,正常退出');
28
18
  process.exit(0);
29
19
  }
30
- const pool = readJson(join(stateDir, 'wish-pool.json'), []);
20
+ const pool = readWishPoolWithFallback(stateRoot);
31
21
  const anchor = buildTimeAnchor(new Date());
32
22
  const ctx = {
33
23
  days: arg('--days') ? Number(arg('--days')) : undefined,
@@ -0,0 +1,31 @@
1
+ import { openSession } from '../capabilities/session/transport.js';
2
+ const t = await openSession({
3
+ mode: 'cdp'
4
+ });
5
+ if (!t.ok) {
6
+ console.log('ATTACH 失败:', t.summary);
7
+ process.exit(1);
8
+ }
9
+ try {
10
+ const all = await t.context.cookies();
11
+ const byDom = {};
12
+ for (const c of all){
13
+ const d = c.domain.replace(/^\./, '');
14
+ if (/ctrip\.com$|meituan\.com$/.test(d)) (byDom[d] ??= []).push(c.name);
15
+ }
16
+ console.log('ATTACH ok,共', all.length, '条 cookie');
17
+ for (const [d, names] of Object.entries(byDom))console.log(d, '=>', names.sort().join(','));
18
+ const ctripNames = new Set(Object.entries(byDom).filter(([d])=>d.includes('ctrip')).flatMap(([, n])=>n));
19
+ const known = [
20
+ 'cticket',
21
+ 'uid',
22
+ 'uname',
23
+ 'passport'
24
+ ].filter((k)=>ctripNames.has(k));
25
+ console.log(known.length > 0 ? `携程登录态:在(${known.join(',')})` : '携程登录态:未检出(名单待以上输出校准)');
26
+ } finally{
27
+ await t.close();
28
+ }
29
+
30
+
31
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/session-attach-diagnose.ts
@@ -0,0 +1,65 @@
1
+ import { chromium } from "playwright-core";
2
+ const route = process.argv[2] ?? "sha-ljg";
3
+ const date = process.argv[3] ?? "2026-10-01";
4
+ const url = `https://flights.ctrip.com/online/list/oneway-${route}?depdate=${date}`;
5
+ const PROFILE = "/tmp/gotry-session-poc-profile";
6
+ async function main() {
7
+ const ctx = await chromium.launchPersistentContext(PROFILE, {
8
+ channel: "chrome",
9
+ headless: false,
10
+ viewport: {
11
+ width: 1440,
12
+ height: 900
13
+ },
14
+ args: [
15
+ "--disable-blink-features=AutomationControlled"
16
+ ]
17
+ });
18
+ const page = ctx.pages()[0] ?? await ctx.newPage();
19
+ const hits = [];
20
+ const cap = (s)=>s.replace(/\s+/g, " ").slice(0, 600);
21
+ ctx.on("response", async (res)=>{
22
+ const u = res.url();
23
+ const ct = res.headers()["content-type"] ?? "";
24
+ if (!ct.includes("json") || u.length > 400) return;
25
+ if (!/search|list|flight|itinerary|poll/i.test(u)) return;
26
+ try {
27
+ const body = await res.text();
28
+ if (body.length < 200) return;
29
+ hits.push({
30
+ url: u,
31
+ bytes: body.length
32
+ });
33
+ if (hits.length <= 3) {
34
+ console.log(`\n[HIT ${hits.length}] ${res.status()} ${cap(u)}\n body(${body.length}B): ${cap(body)}`);
35
+ }
36
+ } catch {}
37
+ });
38
+ console.log(`goto ${url}`);
39
+ await page.goto(url, {
40
+ waitUntil: "domcontentloaded",
41
+ timeout: 30_000
42
+ });
43
+ await page.waitForTimeout(15_000);
44
+ const title = await page.title().catch(()=>"");
45
+ console.log(`\ntitle: ${cap(title)}`);
46
+ const html = await page.content().catch(()=>"");
47
+ const challenged = /验证|滑块|captcha|verify/i.test(title + html.slice(0, 5000));
48
+ console.log(`challenge-detected: ${challenged}`);
49
+ console.log(`\nsummary: ${hits.length} 条搜索类 JSON XHR;总命中 ${hits.reduce((a, b)=>a + b.bytes, 0)}B`);
50
+ console.log("按体积 top5(P1 适配器要嗅探的接口面):");
51
+ for (const h of [
52
+ ...hits
53
+ ].sort((a, b)=>b.bytes - a.bytes).slice(0, 5)){
54
+ console.log(` ${h.bytes}B ${cap(h.url)}`);
55
+ }
56
+ console.log(challenged ? "结论:触发风控——按红线不重试不绕过,如实记录" : "结论:嗅探链路成立(只读,零交互)");
57
+ await ctx.close();
58
+ }
59
+ main().catch((e)=>{
60
+ console.error("PoC 失败:", e instanceof Error ? e.message : e);
61
+ process.exit(1);
62
+ });
63
+
64
+
65
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/session-attach-poc.ts
@@ -0,0 +1,41 @@
1
+ import { chromium } from 'playwright-core';
2
+ import { readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ const udd = process.env.CHROME_USER_DATA_DIR ?? `${homedir()}/Library/Application Support/Google/Chrome`;
5
+ const [port, wsPath] = readFileSync(`${udd}/DevToolsActivePort`, 'utf8').trim().split('\n');
6
+ const wsUrl = `ws://127.0.0.1:${port.trim()}${wsPath.trim()}`;
7
+ for(let round = 1; round <= 3; round++){
8
+ console.log(`第 ${round}/3 轮连接(60s 长等待——若 Chrome 弹出调试授权框,请点「允许」)...`);
9
+ try {
10
+ const browser = await chromium.connectOverCDP(wsUrl, {
11
+ timeout: 60_000
12
+ });
13
+ console.log('ATTACH 成功!');
14
+ const context = browser.contexts()[0];
15
+ const all = await context.cookies();
16
+ const byDom = {};
17
+ for (const c of all){
18
+ const d = c.domain.replace(/^\./, '');
19
+ if (/ctrip\.com$|meituan\.com$/.test(d)) (byDom[d] ??= []).push(c.name);
20
+ }
21
+ console.log(`共 ${all.length} 条 cookie;相关域:`);
22
+ for (const [d, names] of Object.entries(byDom))console.log(' ', d, '=>', names.sort().join(','));
23
+ const ctrip = new Set(Object.entries(byDom).filter(([d])=>d.includes('ctrip')).flatMap(([, n])=>n));
24
+ const hit = [
25
+ 'cticket',
26
+ 'uid',
27
+ 'uname',
28
+ 'passport'
29
+ ].filter((k)=>ctrip.has(k));
30
+ console.log(hit.length ? `携程登录态:在(${hit.join(',')})` : '携程登录态:未检出(用上面名单校准 LOGIN_COOKIE_NAMES)');
31
+ await browser.close().catch(()=>{});
32
+ process.exit(0);
33
+ } catch (e) {
34
+ console.log(` 失败:${e instanceof Error ? e.message.split('\n')[0] : String(e)}`);
35
+ }
36
+ }
37
+ console.log('三轮均未连上——回 chrome://inspect/#remote-debugging 确认开关仍开,再重跑本脚本。');
38
+ process.exit(1);
39
+
40
+
41
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/session-attach-wait.ts