@danceiny/gotry 0.0.1-rc.7 → 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 (75) hide show
  1. package/README.md +29 -74
  2. package/bin/gotry-inner.js +50 -1
  3. package/bin/gotry-stdio-ask.js +59 -0
  4. package/cordis.gotry-patch.yml +63 -5
  5. package/data/flights_2026.json +7 -4
  6. package/data/hotels_2026.json +102 -15
  7. package/data/time-slot-eval.json +384 -0
  8. package/data/yunnan-pack.json +2 -1
  9. package/dist/capabilities/agent-reach-bridge.py +90 -0
  10. package/dist/capabilities/anything.js +2 -2
  11. package/dist/capabilities/flyai.js +131 -0
  12. package/dist/capabilities/hbcli.js +11 -6
  13. package/dist/capabilities/incident-log.js +2 -1
  14. package/dist/capabilities/session/action-cache.js +120 -0
  15. package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
  16. package/dist/capabilities/session/adapters/meituan-local.js +73 -0
  17. package/dist/capabilities/session/extract.js +40 -0
  18. package/dist/capabilities/session/read-guard.js +56 -0
  19. package/dist/capabilities/session/transport.js +95 -0
  20. package/dist/capabilities/session-search.js +95 -0
  21. package/dist/scripts/action-cache-tests.js +107 -0
  22. package/dist/scripts/agent-reach-tests.js +1 -1
  23. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  24. package/dist/scripts/async-collect.js +26 -7
  25. package/dist/scripts/companion-tests.js +112 -0
  26. package/dist/scripts/hbcli-tests.js +18 -2
  27. package/dist/scripts/ledger-tests.js +344 -0
  28. package/dist/scripts/ledger-workflow-crash.js +42 -0
  29. package/dist/scripts/memory-capture-tests.js +77 -0
  30. package/dist/scripts/memory-decay-tests.js +83 -0
  31. package/dist/scripts/memory-metrics.js +24 -0
  32. package/dist/scripts/nudge-digest.js +83 -0
  33. package/dist/scripts/probe-poi-tests.js +9 -0
  34. package/dist/scripts/replay.js +73 -1
  35. package/dist/scripts/session-attach-diagnose.js +31 -0
  36. package/dist/scripts/session-attach-poc.js +65 -0
  37. package/dist/scripts/session-attach-wait.js +41 -0
  38. package/dist/scripts/session-extract-tests.js +81 -0
  39. package/dist/scripts/session-login.js +48 -0
  40. package/dist/scripts/session-tests.js +162 -0
  41. package/dist/scripts/skeleton-check.js +3 -1
  42. package/dist/scripts/skills-contract-tests.js +85 -0
  43. package/dist/scripts/smoke.js +242 -4
  44. package/dist/scripts/state-cli-tests.js +136 -0
  45. package/dist/scripts/state-cli.js +234 -0
  46. package/dist/scripts/time-eval-tests.js +318 -0
  47. package/dist/scripts/travel-timeline-tests.js +123 -0
  48. package/dist/scripts/unified-tests.js +1 -1
  49. package/dist/src/companions.js +112 -0
  50. package/dist/src/dsh-llm.js +27 -7
  51. package/dist/src/index.js +600 -121
  52. package/dist/src/loop.js +89 -25
  53. package/dist/src/memory-capture.js +40 -0
  54. package/dist/src/memory-decay.js +31 -0
  55. package/dist/src/memory-utility.js +55 -0
  56. package/dist/src/mock-llm.js +6 -1
  57. package/dist/src/slot-spec.js +165 -0
  58. package/dist/src/state-ledger.js +848 -0
  59. package/dist/src/time-anchor.js +100 -0
  60. package/dist/src/tool-packet.js +12 -0
  61. package/dist/src/travel-slots.js +144 -0
  62. package/dist/src/travel-timeline.js +78 -0
  63. package/dist/src/unified.js +3 -1
  64. package/dist/src/wish-pool.js +27 -0
  65. package/package.json +6 -2
  66. package/ts/capabilities/hbcli.ts +6 -6
  67. package/ts/capabilities/incident-log.ts +6 -3
  68. package/ts/package.json +3 -0
  69. package/ts/scripts/skeleton-check.ts +5 -1
  70. package/ts/src/dsh-llm.ts +27 -7
  71. package/ts/src/index.ts +439 -91
  72. package/ts/src/loop.ts +109 -34
  73. package/ts/src/mock-llm.ts +13 -1
  74. package/ts/src/unified.ts +6 -1
  75. package/ts/cordis.gotry-patch.yml +0 -36
@@ -17,7 +17,7 @@ import { reach, reachStatus } from '../capabilities/agent-reach.js';
17
17
  });
18
18
  assert.equal(r.verdict, 'found');
19
19
  assert.ok(r.evidence.includes('[agent-reach:web.read@'), '证据链 [agent-reach:web.read@ts]');
20
- assert.ok(typeof r.data === 'string' && r.data.includes('Example Domain'), 'content Example Domain');
20
+ assert.ok(typeof r.data === 'string' && r.data.length > 200, 'content 非空 markdown');
21
21
  console.log(`2. web.read example.com → found (${r.latencyMs}ms) OK`);
22
22
  }{
23
23
  const r = await reach({
@@ -1,18 +1,37 @@
1
- import { loadAsyncTicket, settleAsyncTicket, collectDeepPlanning } from '../src/loop.js';
1
+ import { appendFileSync } from 'node:fs';
2
+ import { collectDeepPlanning, loadAsyncTicket, makeJournaledSolvePort, settleAsyncTicket } from '../src/loop.js';
2
3
  import { solveUnified } from '../src/unified.js';
4
+ import { openLedgerIfExists } from '../src/state-ledger.js';
3
5
  const ticketId = process.argv[2];
6
+ const stateRoot = process.argv[3] ?? '.';
4
7
  if (!ticketId) {
5
- console.error('用法:npx tsx scripts/async-collect.ts <ticketId>(工单在 gotry-state/async/)');
8
+ console.error('用法:npx tsx scripts/async-collect.ts <ticketId> [stateRoot](权威在账本 workflow_runs,视图在 gotry-state/async/)');
6
9
  process.exit(1);
7
10
  }
8
- const loaded = await loadAsyncTicket(ticketId);
11
+ const loaded = await loadAsyncTicket(ticketId, stateRoot);
9
12
  if (!loaded) {
10
- console.error(`工单 ${ticketId} 不存在(gotry-state/async/${ticketId}.json)`);
13
+ console.error(`工单 ${ticketId} 不存在(账本 workflow_runs / gotry-state/async/${ticketId}.json)`);
11
14
  process.exit(1);
12
15
  }
13
- const { reply } = await collectDeepPlanning(loaded.state, loaded.ticket, solveUnified);
14
- const out = await settleAsyncTicket(ticketId, reply);
15
- console.log(`交付物已落盘:ts/${out}\n\n${reply}`);
16
+ const ledger = openLedgerIfExists(stateRoot);
17
+ const run = ledger?.getWorkflowRun(ticketId);
18
+ if (run?.status === 'settled' && run.deliverable) {
19
+ console.log(`工单 ${ticketId} 已交付(账本终态,复诵不重算):\n\n${run.deliverable}`);
20
+ process.exit(0);
21
+ }
22
+ if (!ledger) {
23
+ console.error(`工单 ${ticketId}:stateRoot(${stateRoot})无账本——持久化先于回收,不应发生`);
24
+ process.exit(1);
25
+ }
26
+ const solve = makeJournaledSolvePort(ledger, ticketId, solveUnified, {
27
+ onRealSolve: ()=>{
28
+ const f = process.env['GOTRY_SOLVE_COUNT_FILE'];
29
+ if (f) appendFileSync(f, 'solve\n');
30
+ }
31
+ });
32
+ const { reply } = await collectDeepPlanning(loaded.state, loaded.ticket, solve);
33
+ const out = await settleAsyncTicket(ticketId, reply, stateRoot);
34
+ console.log(`交付物已落盘:${out}\n\n${reply}`);
16
35
 
17
36
 
18
37
  //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/async-collect.ts
@@ -0,0 +1,112 @@
1
+ import assert from 'node:assert/strict';
2
+ import { companionId, sensitiveViolation, upsertCompanion } from '../src/companions.js';
3
+ let n = 0;
4
+ function pass(name, body) {
5
+ body();
6
+ console.log(` ${++n}. ${name} OK`);
7
+ }
8
+ pass('负面清单守卫:证件号/手机号拒收入库(红线 6 工程形态)', ()=>{
9
+ assert.ok(sensitiveViolation('护照号 E12345678')?.includes('负面清单'), '证件拒');
10
+ assert.ok(sensitiveViolation('我电话 13812345678')?.includes('负面清单'), '手机号拒');
11
+ assert.equal(sensitiveViolation('爸爸65,轻度高血压,走不动山路'), null, '行为约束正常');
12
+ const r = upsertCompanion([], {
13
+ label: '爸爸',
14
+ constraints: {
15
+ health: [
16
+ '轻度高血压'
17
+ ]
18
+ },
19
+ evidence: '身份证号 110101199001011234,轻度高血压'
20
+ });
21
+ assert.equal(r.appended, false);
22
+ assert.ok(r.reason?.includes('负面清单'));
23
+ });
24
+ pass('新建:约束+证据落库,companion_id 语义派生', ()=>{
25
+ const r = upsertCompanion([], {
26
+ label: '爸 爸',
27
+ constraints: {
28
+ health: [
29
+ '轻度高血压'
30
+ ],
31
+ mobility: '步行≤4h'
32
+ },
33
+ evidence: '爸爸65有轻度高血压,别太累'
34
+ });
35
+ assert.equal(r.appended, true);
36
+ assert.equal(r.companionId, '爸爸');
37
+ const p = r.profiles[0];
38
+ assert.deepEqual(p.constraints.health, [
39
+ '轻度高血压'
40
+ ]);
41
+ assert.deepEqual(p.evidence, [
42
+ '爸爸65有轻度高血压,别太累'
43
+ ]);
44
+ });
45
+ pass('upsert 合并:数组追加不删史/覆盖取新/幂等', ()=>{
46
+ let ps = [];
47
+ ps = upsertCompanion(ps, {
48
+ label: '爸爸',
49
+ constraints: {
50
+ health: [
51
+ '轻度高血压'
52
+ ],
53
+ mobility: '步行≤4h'
54
+ },
55
+ evidence: '原话一'
56
+ }).profiles;
57
+ ps = upsertCompanion(ps, {
58
+ label: '爸爸',
59
+ constraints: {
60
+ health: [
61
+ '晕车'
62
+ ],
63
+ mobility: '步行≤3h'
64
+ },
65
+ evidence: '原话二:晕车'
66
+ }).profiles;
67
+ const p = ps.find((x)=>x.companion_id === '爸爸');
68
+ assert.deepEqual(p.constraints.health, [
69
+ '轻度高血压',
70
+ '晕车'
71
+ ], '数组追加不删史');
72
+ assert.equal(p.constraints.mobility, '步行≤3h', '标量取新值');
73
+ assert.equal(p.evidence.length, 2, '证据两条');
74
+ const r3 = upsertCompanion(ps, {
75
+ label: '爸爸',
76
+ constraints: {
77
+ health: [
78
+ '晕车'
79
+ ],
80
+ mobility: '步行≤3h'
81
+ },
82
+ evidence: '原话二:晕车'
83
+ });
84
+ assert.equal(r3.appended, false, '全量相同幂等 no-op');
85
+ });
86
+ pass('多人并存与 id 稳定性', ()=>{
87
+ let ps = [];
88
+ ps = upsertCompanion(ps, {
89
+ label: '爸爸',
90
+ constraints: {
91
+ health: [
92
+ '高血压'
93
+ ]
94
+ },
95
+ evidence: 'a'
96
+ }).profiles;
97
+ ps = upsertCompanion(ps, {
98
+ label: '女朋友',
99
+ constraints: {
100
+ prefs: [
101
+ '怕吵'
102
+ ]
103
+ },
104
+ evidence: 'b'
105
+ }).profiles;
106
+ assert.equal(ps.length, 2);
107
+ assert.equal(companionId('女 朋友'), '女朋友');
108
+ });
109
+ console.log(`\nCOMPANION TESTS: ${n}/4 OK(memory-design P2 守门面)`);
110
+
111
+
112
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/companion-tests.ts
@@ -66,7 +66,23 @@ await writeFile(fallback, JSON.stringify({
66
66
  }
67
67
  ]
68
68
  }));
69
- const r4 = await searchHotels({
69
+ {
70
+ const { writeFileSync, chmodSync } = await import('node:fs');
71
+ const echoBin = join(tmp, 'hbcli-echo');
72
+ writeFileSync(echoBin, '#!/bin/sh\necho "ARGS:$@"; echo \'{}\' ');
73
+ chmodSync(echoBin, 0o755);
74
+ const re = await searchHotels({
75
+ destination: '普吉',
76
+ adults: 2
77
+ }, {
78
+ hbcliBin: echoBin
79
+ });
80
+ const argLine = JSON.stringify(re);
81
+ if (!argLine.includes('destination-name') || !argLine.includes('room-occupancies')) {
82
+ throw new Error(`FAIL: v0.3.0 旗标未对齐,实际 ${argLine.slice(0, 200)}`);
83
+ }
84
+ console.log('5. v0.3.0 旗标(--destination-name/--room-occupancies)对齐 OK');
85
+ }const r4 = await searchHotels({
70
86
  destination: '普吉'
71
87
  }, {
72
88
  hbcliBin: failBin2,
@@ -79,7 +95,7 @@ await rm(tmp, {
79
95
  recursive: true,
80
96
  force: true
81
97
  });
82
- console.log('HBCLI TESTS: 4/4 OK (happy path / error path / no-binary / fallback)');
98
+ console.log('HBCLI TESTS: 5/5 OK (happy / error / no-binary / fallback / v0.3.0 旗标回归)');
83
99
 
84
100
 
85
101
  //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/hbcli-tests.ts
@@ -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,77 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mergeProfile } from '../src/memory-capture.js';
3
+ const cur = {
4
+ weights: {
5
+ escape_rest: 1
6
+ },
7
+ evidence: [
8
+ '用户原话:「想去湖边什么都不干」'
9
+ ],
10
+ hard: {
11
+ wake_not_before: '07:00'
12
+ }
13
+ };
14
+ {
15
+ const m1 = mergeProfile(cur, {
16
+ evidence: [
17
+ '用户原话:「预算 5000 以内」',
18
+ '用户原话:「想去湖边什么都不干」'
19
+ ]
20
+ });
21
+ assert.equal(m1?.evidence.length, 2, `追加新+跳过重复,实际 ${m1?.evidence.length}`);
22
+ assert.ok(m1?.evidence[0].includes('湖边'), '既有 evidence 原位保留(P0)');
23
+ const m2 = mergeProfile(m1, {
24
+ evidence: [
25
+ '用户原话:「预算 5000 以内」'
26
+ ]
27
+ });
28
+ assert.equal(m2, null, '纯重复补丁应返 null(幂等)');
29
+ console.log('1. 追加不删史 + 幂等 OK');
30
+ }{
31
+ const m = mergeProfile(cur, {
32
+ weights: {
33
+ escape_rest: 0.5,
34
+ curiosity: 0.5
35
+ },
36
+ evidence: [
37
+ '用户原话:「这次既要躺平也要探索」'
38
+ ]
39
+ });
40
+ const sum = Object.values(m?.weights ?? {}).reduce((a, b)=>a + b, 0);
41
+ assert.ok(Math.abs(sum - 1) < 0.01, `权重应归一,实际 sum=${sum}`);
42
+ const noEv = mergeProfile(cur, {
43
+ weights: {
44
+ escape_rest: 0.3,
45
+ curiosity: 0.7
46
+ }
47
+ });
48
+ assert.equal(noEv?.weights.escape_rest, 1, '权重变更无新 evidence 应被拒(P0)');
49
+ console.log('2. 权重归一 + P0 证据校验 OK');
50
+ }{
51
+ const first = mergeProfile(null, {
52
+ weights: {
53
+ escape_rest: 1
54
+ },
55
+ evidence: [
56
+ '用户原话:「想去湖边」'
57
+ ]
58
+ });
59
+ assert.ok(first && first.evidence.length === 1 && Object.keys(first.weights).length === 1, '首存应生效');
60
+ assert.equal(mergeProfile(null, null), null, 'null 补丁仍守卫');
61
+ console.log('2.5 首存语义(current=null→空档案) OK');
62
+ }{
63
+ assert.equal(mergeProfile(cur, null), null, 'null 补丁');
64
+ assert.equal(mergeProfile(cur, {}), null, '空补丁');
65
+ const m = mergeProfile(cur, {
66
+ hard: {
67
+ wake_not_before: '08:30',
68
+ budget_cny: 5000
69
+ }
70
+ });
71
+ assert.equal(m?.hard?.wake_not_before, '08:30', 'hard 后到优先');
72
+ assert.equal(m?.hard?.budget_cny, 5000, 'hard 新键并入');
73
+ console.log('3. 空守卫 + hard 覆盖后到优先 OK');
74
+ }console.log('\nMEMORY-MERGE TESTS: 3/3 OK(T1 合并守门层)');
75
+
76
+
77
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/memory-capture-tests.ts