@danceiny/gotry 0.2.0-rc.32 → 0.2.0-rc.33

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 (89) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/dist/capabilities/session/bridge-ledger.js +168 -67
  4. package/dist/capabilities/session/extension-bridge.js +140 -46
  5. package/dist/capabilities/session/gui-flight.js +281 -69
  6. package/dist/capabilities/session/gui-hotel.js +74 -17
  7. package/dist/capabilities/session-search.js +6 -5
  8. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +129 -2
  9. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +53 -5
  10. package/dist/scripts/booking-copilot-gateway-chat-adapter-tests.js +233 -0
  11. package/dist/scripts/booking-copilot-planner-live-probe.js +1 -0
  12. package/dist/scripts/booking-copilot-startup-proof-tests.js +1 -1
  13. package/dist/scripts/bridge-ledger-tests.js +20 -18
  14. package/dist/scripts/fixtures/tenant-runaway-worker-fixture.mjs +73 -0
  15. package/dist/scripts/mutation-check.js +153 -0
  16. package/dist/scripts/nightly-evidence.js +4 -111
  17. package/dist/scripts/session-tests.js +42 -8
  18. package/dist/scripts/tenant-bridge-org-tests.js +1264 -0
  19. package/dist/scripts/tenant-capacity-probe.js +263 -0
  20. package/dist/scripts/tenant-compute-isolation-tests.js +707 -0
  21. package/dist/scripts/tenant-egress-credentials-tests.js +822 -0
  22. package/dist/scripts/tenant-executor-binding-tests.js +203 -0
  23. package/dist/scripts/tenant-prompt-partition-tests.js +265 -0
  24. package/dist/scripts/tenant-seam-adversarial-tests.js +957 -0
  25. package/dist/scripts/tenant-seam-quota-tests.js +383 -0
  26. package/dist/scripts/tenant-seam-test-kit.js +292 -0
  27. package/dist/scripts/tenant-state-copies-tests.js +607 -0
  28. package/dist/scripts/tenant-state-drill-tests.js +298 -0
  29. package/dist/scripts/tenant-state-drill-worker.js +164 -0
  30. package/dist/scripts/tenant-state-durable-conformance.js +101 -0
  31. package/dist/scripts/tenant-state-handoff-tests.js +854 -0
  32. package/dist/scripts/tenant-state-lease-conformance.js +142 -0
  33. package/dist/scripts/tenant-state-lease-worker.js +28 -0
  34. package/dist/scripts/tenant-state-router-tests.js +464 -0
  35. package/dist/scripts/tenant-state-test-kit.js +92 -0
  36. package/dist/scripts/tenant-state-tests.js +564 -0
  37. package/dist/src/backend/modules/booking-executor.js +61 -6
  38. package/dist/src/backend/modules/session-search.js +136 -39
  39. package/dist/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
  40. package/dist/src/booking-surface/dsh-planner.js +150 -19
  41. package/dist/src/booking-surface/index.js +1 -0
  42. package/dist/src/booking-surface/managed-dsh-run-port.js +121 -7
  43. package/dist/src/booking-surface/server.js +193 -80
  44. package/dist/src/booking-surface/startup.js +153 -0
  45. package/dist/src/booking-surface/tenancy.js +188 -0
  46. package/dist/src/index.js +31 -1
  47. package/dist/src/llm-price-table.js +148 -0
  48. package/dist/src/tenant-seam/index.js +8 -0
  49. package/dist/src/tenant-seam/llm-broker.js +508 -0
  50. package/dist/src/tenant-seam/process-governor.js +281 -0
  51. package/dist/src/tenant-seam/quotas.js +360 -0
  52. package/dist/src/tenant-seam/seam.js +92 -0
  53. package/dist/src/tenant-state/durable-store.js +158 -0
  54. package/dist/src/tenant-state/epoch-copies.js +422 -0
  55. package/dist/src/tenant-state/errors.js +108 -0
  56. package/dist/src/tenant-state/ids.js +101 -0
  57. package/dist/src/tenant-state/index.js +11 -0
  58. package/dist/src/tenant-state/lease-store.js +203 -0
  59. package/dist/src/tenant-state/tenant-lease.js +150 -0
  60. package/dist/src/tenant-state/tenant-router.js +650 -0
  61. package/extension/manifest.json +2 -2
  62. package/package.json +4 -1
  63. package/ts/capabilities/session/bridge-ledger.ts +192 -78
  64. package/ts/capabilities/session/extension-bridge.ts +176 -65
  65. package/ts/capabilities/session/gui-flight.ts +318 -88
  66. package/ts/capabilities/session/gui-hotel.ts +72 -18
  67. package/ts/capabilities/session-search.ts +17 -5
  68. package/ts/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
  69. package/ts/src/booking-surface/dsh-planner.ts +169 -19
  70. package/ts/src/booking-surface/index.ts +1 -0
  71. package/ts/src/booking-surface/managed-dsh-run-port.ts +150 -7
  72. package/ts/src/booking-surface/server.ts +215 -84
  73. package/ts/src/booking-surface/startup.ts +155 -0
  74. package/ts/src/booking-surface/tenancy.ts +242 -0
  75. package/ts/src/index.ts +28 -2
  76. package/ts/src/llm-price-table.ts +180 -0
  77. package/ts/src/tenant-seam/index.ts +9 -0
  78. package/ts/src/tenant-seam/llm-broker.ts +520 -0
  79. package/ts/src/tenant-seam/process-governor.ts +343 -0
  80. package/ts/src/tenant-seam/quotas.ts +433 -0
  81. package/ts/src/tenant-seam/seam.ts +142 -0
  82. package/ts/src/tenant-state/durable-store.ts +171 -0
  83. package/ts/src/tenant-state/epoch-copies.ts +491 -0
  84. package/ts/src/tenant-state/errors.ts +128 -0
  85. package/ts/src/tenant-state/ids.ts +108 -0
  86. package/ts/src/tenant-state/index.ts +12 -0
  87. package/ts/src/tenant-state/lease-store.ts +266 -0
  88. package/ts/src/tenant-state/tenant-lease.ts +175 -0
  89. package/ts/src/tenant-state/tenant-router.ts +743 -0
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Capacity probe of the AaaS tier (docs/rfc/aaas-rfc.md §6 switch trigger 4; docs/ops/aaas-capacity-baseline.md).
3
+ *
4
+ * The RFC's fallback to a different storage design depends on "tenants per node times the cost of a file", and until now
5
+ * nothing measured either. This probe measures, on the machine it runs on and with the real code, the three things that
6
+ * decide how many tenants a replica can hold:
7
+ *
8
+ * density what N open tenants cost in resident memory, file descriptors and time to open, and whether closing them
9
+ * gives the handles back
10
+ * restore how long it takes a new owner to take a tenant over from the durable copy, as the ledger grows
11
+ * planner what one booted planner process tree costs idle, and how long it takes to boot (no model call is made)
12
+ *
13
+ * It is a measurement, not a verdict. The durable store here is a local directory, so restore latency excludes the
14
+ * network and the object store; the planner is booted against an address nothing listens on. A node that matters
15
+ * has to be probed itself (docs/ops/aaas-capacity-baseline.md says what is still a human probe).
16
+ *
17
+ * cd ts && npx tsx scripts/tenant-capacity-probe.ts [--smoke] [--json <file>] [--only density|restore|planner] [--tenants 1,10,100] [--sizes-mb 1,10] [--no-planner]
18
+ *
19
+ * `--smoke` is the regression form: small numbers, and it asserts correctness only (everything opened, handles returned,
20
+ * a restored ledger holds what was written), never a speed.
21
+ */
22
+ import assert from 'node:assert/strict';
23
+ import { cpus, platform, arch, totalmem } from 'node:os';
24
+ import { mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
25
+ import { tmpdir } from 'node:os';
26
+ import { join } from 'node:path';
27
+ import { execFileSync } from 'node:child_process';
28
+ import { createRealRunPort } from '../src/booking-surface/dsh-planner.js';
29
+ import { DirDurableStore } from '../src/tenant-state/durable-store.js';
30
+ import { deriveLedgerTenantId } from '../src/tenant-state/ids.js';
31
+ import { SqliteLeaseStore } from '../src/tenant-state/lease-store.js';
32
+ import { TenantRouter } from '../src/tenant-state/tenant-router.js';
33
+ import { parsePsTable, treeOf } from '../src/tenant-seam/process-governor.js';
34
+ const args = process.argv.slice(2);
35
+ const flag = (name) => args.includes(name);
36
+ const value = (name) => {
37
+ const at = args.indexOf(name);
38
+ return at >= 0 ? args[at + 1] : undefined;
39
+ };
40
+ const list = (name, fallback) => {
41
+ const raw = value(name);
42
+ if (raw === undefined)
43
+ return fallback;
44
+ const parsed = raw.split(',').map(Number);
45
+ if (parsed.some((n) => !Number.isFinite(n) || n <= 0))
46
+ throw new Error(`${name} takes a comma-separated list of positive numbers`);
47
+ return parsed;
48
+ };
49
+ const ONLY = value('--only');
50
+ if (ONLY !== undefined && !['density', 'restore', 'planner'].includes(ONLY))
51
+ throw new Error('--only takes density, restore or planner');
52
+ const SMOKE = flag('--smoke');
53
+ const TENANT_STEPS = list('--tenants', SMOKE ? [5, 20] : [1, 10, 50, 100, 250, 500]);
54
+ const SIZES_MB = list('--sizes-mb', SMOKE ? [1] : [1, 10, 50, 100]);
55
+ const REPEATS = SMOKE ? 1 : 3;
56
+ const WITH_PLANNER = !SMOKE && !flag('--no-planner');
57
+ const MIB = 1024 * 1024;
58
+ const ORG = 'probe';
59
+ const KEY = Buffer.alloc(32, 0x5a);
60
+ const ISSUER = 'https://idp.probe.example';
61
+ const median = (values) => {
62
+ const sorted = [...values].sort((a, b) => a - b);
63
+ return sorted.length === 0 ? Number.NaN : sorted[Math.floor((sorted.length - 1) / 2)];
64
+ };
65
+ const percentile = (values, p) => {
66
+ const sorted = [...values].sort((a, b) => a - b);
67
+ return sorted.length === 0 ? Number.NaN : sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * p) - 1)];
68
+ };
69
+ const round = (n, digits = 1) => Number(n.toFixed(digits));
70
+ const mib = (bytes) => round(bytes / MIB);
71
+ function fdCount() {
72
+ return readdirSync('/dev/fd').length;
73
+ }
74
+ function tenantIdOf(i) {
75
+ return deriveLedgerTenantId(KEY, ISSUER, `probe-user-${i}`);
76
+ }
77
+ const scratch = mkdtempSync(join(tmpdir(), 'gotry-capacity-probe-'));
78
+ const cleanups = [];
79
+ function fleet(name) {
80
+ const dir = join(scratch, name);
81
+ const leases = new SqliteLeaseStore({ path: join(dir, 'leases.db') });
82
+ const durable = new DirDurableStore(join(dir, 'durable'));
83
+ cleanups.push(() => leases.close());
84
+ const router = (replica) => new TenantRouter({ stateRoot: join(dir, 'nodes', replica), replicaId: replica, leases, durable, ttlMs: 60_000, marginMs: 2_000, autoRenew: false, snapshotEveryMs: 0 });
85
+ return { router };
86
+ }
87
+ // ---- density -------------------------------------------------------------------------------------------------
88
+ async function density() {
89
+ const { router: make } = fleet('density');
90
+ const router = make('density-replica');
91
+ const baseFds = fdCount();
92
+ const baseRss = process.memoryUsage().rss;
93
+ const rows = [];
94
+ const openTimes = [];
95
+ let opened = 0;
96
+ for (const step of TENANT_STEPS) {
97
+ for (; opened < step; opened += 1) {
98
+ const started = performance.now();
99
+ await router.openTenant(ORG, tenantIdOf(opened));
100
+ openTimes.push(performance.now() - started);
101
+ }
102
+ global.gc?.();
103
+ const memory = process.memoryUsage();
104
+ rows.push({
105
+ tenants: step,
106
+ rssMiB: mib(memory.rss),
107
+ rssDeltaMiB: mib(memory.rss - baseRss),
108
+ heapUsedMiB: mib(memory.heapUsed),
109
+ externalMiB: mib(memory.external),
110
+ fds: fdCount(),
111
+ fdDelta: fdCount() - baseFds,
112
+ openMsMedian: round(median(openTimes), 2),
113
+ openMsP95: round(percentile(openTimes, 0.95), 2),
114
+ });
115
+ }
116
+ const closing = performance.now();
117
+ const unflushed = await router.close();
118
+ const closeMs = performance.now() - closing;
119
+ return { rows, afterClose: { fds: fdCount(), fdDelta: fdCount() - baseFds, closeMs: round(closeMs), unflushed: unflushed.length } };
120
+ }
121
+ // ---- restore -------------------------------------------------------------------------------------------------
122
+ async function restoreOnce(sizeMiB, run) {
123
+ const { router: make } = fleet(`restore-${sizeMiB}-${run}`);
124
+ const owner = make('owner');
125
+ const successor = make('successor');
126
+ const tenant = tenantIdOf(1_000_000 + run);
127
+ const handle = await owner.openTenant(ORG, tenant);
128
+ const rowsToWrite = Math.max(1, Math.round((sizeMiB * MIB) / 65_536));
129
+ handle.run((ledger) => {
130
+ ledger.db.exec('CREATE TABLE IF NOT EXISTS capacity_probe (id INTEGER PRIMARY KEY, payload BLOB NOT NULL)');
131
+ const insert = ledger.db.prepare('INSERT INTO capacity_probe(payload) VALUES (randomblob(65536))');
132
+ ledger.db.transaction(() => { for (let i = 0; i < rowsToWrite; i += 1)
133
+ insert.run(); })();
134
+ });
135
+ const snapshotting = performance.now();
136
+ await handle.handoff();
137
+ const snapshotMs = performance.now() - snapshotting;
138
+ const restoring = performance.now();
139
+ const taken = await successor.openTenant(ORG, tenant);
140
+ const restoreMs = performance.now() - restoring;
141
+ const restoredRows = taken.run((ledger) => ledger.db.prepare('SELECT COUNT(*) AS n FROM capacity_probe').get().n);
142
+ const sizeBytes = taken.run((ledger) => {
143
+ const pages = ledger.db.pragma('page_count', { simple: true });
144
+ const pageSize = ledger.db.pragma('page_size', { simple: true });
145
+ return pages * pageSize;
146
+ });
147
+ await taken.handoff();
148
+ await owner.close();
149
+ await successor.close();
150
+ return { ledgerMiB: mib(sizeBytes), snapshotMs: round(snapshotMs), restoreMs: round(restoreMs), restoredRows, writtenRows: rowsToWrite };
151
+ }
152
+ async function restore() {
153
+ const out = [];
154
+ for (const size of SIZES_MB) {
155
+ const runs = [];
156
+ for (let run = 0; run < REPEATS; run += 1)
157
+ runs.push(await restoreOnce(size, run));
158
+ const middle = runs.sort((a, b) => a.restoreMs - b.restoreMs)[Math.floor((runs.length - 1) / 2)];
159
+ out.push({ requestedMiB: size, ...middle });
160
+ }
161
+ return out;
162
+ }
163
+ // ---- planner -------------------------------------------------------------------------------------------------
164
+ /** The process trees below this process, measured as they are now. The `ps` that reads the table is itself a child and is left out. */
165
+ function ownTreeSample() {
166
+ const text = execFileSync('/bin/ps', ['-A', '-o', 'pid=,ppid=,rss=,time=,command='], { encoding: 'utf8', env: { PATH: '/usr/bin:/bin', LC_ALL: 'C' } });
167
+ const withoutPs = text.split('\n').filter((line) => !/^\s*\d+\s+\d+\s+\d+\s+\S+\s+(?:\S*\/)?ps(?:\s|$)/.test(line)).join('\n');
168
+ const table = parsePsTable(withoutPs.replace(/^(\s*\d+\s+\d+\s+\d+\s+\S+)\s+.*$/gm, '$1'));
169
+ let processes = 0;
170
+ let rssBytes = 0;
171
+ let cpuSeconds = 0;
172
+ for (const child of table.filter((row) => row.ppid === process.pid)) {
173
+ const tree = treeOf(table, { rootPid: child.pid, parentPid: process.pid });
174
+ if (!tree)
175
+ continue;
176
+ processes += tree.pids.length;
177
+ rssBytes += tree.rssBytes;
178
+ cpuSeconds += tree.cpuSeconds;
179
+ }
180
+ return { processes, rssBytes, cpuSeconds };
181
+ }
182
+ async function planner() {
183
+ const rows = [];
184
+ process.env.GOTRY_BOOKING_COPILOT_WARMUP = '0';
185
+ for (const trees of [1, 4]) {
186
+ const stateRoot = mkdtempSync(join(scratch, 'planner-'));
187
+ const ports = await Promise.all(Array.from({ length: trees }, () => createRealRunPort({
188
+ stateRoot,
189
+ env: { PATH: process.env.PATH, DEEPSEEK_API_KEY: 'probe-key-not-used', DEEPSEEK_BASE_URL: 'http://127.0.0.1:9/v1', DEEPSEEK_PROVIDER: 'gateway-chat', DEEPSEEK_MODEL: 'probe-model' },
190
+ })));
191
+ const booting = performance.now();
192
+ await Promise.all(ports.map((port) => port.warmup()));
193
+ const bootMs = performance.now() - booting;
194
+ await new Promise((resolve) => setTimeout(resolve, 1_500));
195
+ const sample = ownTreeSample();
196
+ rows.push({
197
+ trees,
198
+ processes: sample.processes,
199
+ rssMiB: mib(sample.rssBytes),
200
+ rssPerTreeMiB: mib(sample.rssBytes / trees),
201
+ cpuSeconds: round(sample.cpuSeconds, 2),
202
+ bootMs: round(bootMs),
203
+ });
204
+ await Promise.all(ports.map((port) => port.close()));
205
+ }
206
+ return rows;
207
+ }
208
+ // ---- run -------------------------------------------------------------------------------------------------
209
+ const environment = {
210
+ date: new Date().toISOString().slice(0, 10),
211
+ node: process.version,
212
+ platform: `${platform()}/${arch()}`,
213
+ cpu: `${cpus()[0]?.model ?? 'unknown'} x${cpus().length}`,
214
+ memoryGiB: round(totalmem() / (1024 * MIB)),
215
+ durableStore: 'local directory (no network, no object store)',
216
+ mode: SMOKE ? 'smoke' : 'full',
217
+ };
218
+ try {
219
+ const densityResult = ONLY === undefined || ONLY === 'density' ? await density() : { rows: [], afterClose: { fds: 0, fdDelta: 0, closeMs: 0, unflushed: 0 } };
220
+ const restoreResult = ONLY === undefined || ONLY === 'restore' ? await restore() : [];
221
+ const plannerResult = WITH_PLANNER && (ONLY === undefined || ONLY === 'planner') ? await planner() : [];
222
+ const report = { environment, density: densityResult, restore: restoreResult, planner: plannerResult };
223
+ if (SMOKE) {
224
+ const last = densityResult.rows.at(-1);
225
+ assert.equal(last.tenants, TENANT_STEPS.at(-1), 'every tenant opened');
226
+ assert.ok(densityResult.afterClose.fdDelta <= 8, `closing the tenants returned their handles (fd delta ${densityResult.afterClose.fdDelta})`);
227
+ assert.equal(densityResult.afterClose.unflushed, 0, 'every tenant flushed on close');
228
+ for (const row of restoreResult) {
229
+ assert.ok(row.writtenRows > 0 && row.restoredRows === row.writtenRows, `a restored ledger holds exactly what was written (${row.restoredRows} of ${row.writtenRows})`);
230
+ assert.ok(Number.isFinite(row.restoreMs) && row.restoreMs >= 0 && Number.isFinite(row.snapshotMs));
231
+ }
232
+ console.log(`tenant-capacity-probe: smoke ok (${last.tenants} tenants, fd delta after close ${densityResult.afterClose.fdDelta}, ${restoreResult.length} restore sizes)`);
233
+ }
234
+ else {
235
+ console.log(`environment: ${JSON.stringify(environment)}`);
236
+ if (densityResult.rows.length > 0) {
237
+ console.log('\ndensity (one process, tenants opened one after another):');
238
+ console.table(densityResult.rows);
239
+ console.log(`after close: ${JSON.stringify(densityResult.afterClose)}`);
240
+ }
241
+ if (restoreResult.length > 0) {
242
+ console.log('\nrestore (a new owner takes over from the durable copy; median of ' + REPEATS + '):');
243
+ console.table(restoreResult);
244
+ }
245
+ if (plannerResult.length > 0) {
246
+ console.log('\nplanner (booted, idle, no model call, warm-up off):');
247
+ console.table(plannerResult);
248
+ }
249
+ }
250
+ const out = value('--json');
251
+ if (out)
252
+ writeFileSync(out, `${JSON.stringify(report, null, 2)}\n`);
253
+ }
254
+ finally {
255
+ for (const cleanup of cleanups) {
256
+ try {
257
+ cleanup();
258
+ }
259
+ catch { /* best effort */ }
260
+ }
261
+ rmSync(scratch, { recursive: true, force: true });
262
+ }
263
+ process.exit(0);