@danceiny/gotry 0.2.0-rc.31 → 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.
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/gotry-bootstrap.js +63 -53
- package/bin/gotry-hbcli-release.d.ts +3 -0
- package/bin/gotry-hbcli-release.js +4 -0
- package/client/client.js +66 -21
- package/dist/capabilities/doctor.js +7 -8
- package/dist/capabilities/effect.js +45 -12
- package/dist/capabilities/flyai-config.js +12 -10
- package/dist/capabilities/flyai.js +87 -56
- package/dist/capabilities/hbcli-config.js +156 -0
- package/dist/capabilities/hbcli.js +29 -2
- package/dist/capabilities/resilience.js +6 -6
- package/dist/capabilities/session/adapters/ctrip-flight.js +42 -7
- package/dist/capabilities/session/adapters/ctrip-hotel.js +52 -3
- package/dist/capabilities/session/bridge-ledger.js +168 -67
- package/dist/capabilities/session/ctrip-city.js +186 -0
- package/dist/capabilities/session/extension-bridge.js +148 -47
- package/dist/capabilities/session/gui-flight.js +949 -0
- package/dist/capabilities/session/gui-hotel.js +470 -0
- package/dist/capabilities/session-search.js +102 -14
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +129 -2
- package/dist/scripts/booking-copilot-dsh-readiness-proof.js +53 -5
- package/dist/scripts/booking-copilot-gateway-chat-adapter-tests.js +233 -0
- package/dist/scripts/booking-copilot-planner-live-probe.js +1 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +1 -1
- package/dist/scripts/bootstrap-tests.js +23 -6
- package/dist/scripts/bridge-ledger-tests.js +20 -18
- package/dist/scripts/ctrip-city-tests.js +80 -0
- package/dist/scripts/effect-tests.js +602 -504
- package/dist/scripts/extension-tests.js +14 -8
- package/dist/scripts/fixtures/tenant-runaway-worker-fixture.mjs +73 -0
- package/dist/scripts/flight-malformed-tests.js +1 -1
- package/dist/scripts/flyai-setup-tests.js +19 -4
- package/dist/scripts/flyai-tests.js +768 -653
- package/dist/scripts/gotry-web-api-tests.js +5 -0
- package/dist/scripts/gotry-web-client-tests.js +20 -1
- package/dist/scripts/gui-flight-live.js +27 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/hbcli-upgrade-tests.js +107 -0
- package/dist/scripts/hbcli-web-setup-tests.js +114 -0
- package/dist/scripts/hotel-date-gate-tests.js +1 -1
- package/dist/scripts/hotel-result-tests.js +169 -0
- package/dist/scripts/issue-271-liveness-tests.js +1 -1
- package/dist/scripts/mutation-check.js +153 -0
- package/dist/scripts/nightly-evidence.js +4 -111
- package/dist/scripts/session-tests.js +211 -6
- package/dist/scripts/tenant-bridge-org-tests.js +1264 -0
- package/dist/scripts/tenant-capacity-probe.js +263 -0
- package/dist/scripts/tenant-compute-isolation-tests.js +707 -0
- package/dist/scripts/tenant-egress-credentials-tests.js +822 -0
- package/dist/scripts/tenant-executor-binding-tests.js +203 -0
- package/dist/scripts/tenant-prompt-partition-tests.js +265 -0
- package/dist/scripts/tenant-seam-adversarial-tests.js +957 -0
- package/dist/scripts/tenant-seam-quota-tests.js +383 -0
- package/dist/scripts/tenant-seam-test-kit.js +292 -0
- package/dist/scripts/tenant-state-copies-tests.js +607 -0
- package/dist/scripts/tenant-state-drill-tests.js +298 -0
- package/dist/scripts/tenant-state-drill-worker.js +164 -0
- package/dist/scripts/tenant-state-durable-conformance.js +101 -0
- package/dist/scripts/tenant-state-handoff-tests.js +854 -0
- package/dist/scripts/tenant-state-lease-conformance.js +142 -0
- package/dist/scripts/tenant-state-lease-worker.js +28 -0
- package/dist/scripts/tenant-state-router-tests.js +464 -0
- package/dist/scripts/tenant-state-test-kit.js +92 -0
- package/dist/scripts/tenant-state-tests.js +564 -0
- package/dist/src/backend/modules/booking-executor.js +61 -6
- package/dist/src/backend/modules/session-search.js +136 -39
- package/dist/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
- package/dist/src/booking-surface/dsh-planner.js +150 -19
- package/dist/src/booking-surface/index.js +1 -0
- package/dist/src/booking-surface/managed-dsh-run-port.js +121 -7
- package/dist/src/booking-surface/server.js +193 -80
- package/dist/src/booking-surface/startup.js +153 -0
- package/dist/src/booking-surface/tenancy.js +188 -0
- package/dist/src/gotry-web-api.js +64 -2
- package/dist/src/index.js +70 -18
- package/dist/src/llm-price-table.js +148 -0
- package/dist/src/tenant-seam/index.js +8 -0
- package/dist/src/tenant-seam/llm-broker.js +508 -0
- package/dist/src/tenant-seam/process-governor.js +281 -0
- package/dist/src/tenant-seam/quotas.js +360 -0
- package/dist/src/tenant-seam/seam.js +92 -0
- package/dist/src/tenant-state/durable-store.js +158 -0
- package/dist/src/tenant-state/epoch-copies.js +422 -0
- package/dist/src/tenant-state/errors.js +108 -0
- package/dist/src/tenant-state/ids.js +101 -0
- package/dist/src/tenant-state/index.js +11 -0
- package/dist/src/tenant-state/lease-store.js +203 -0
- package/dist/src/tenant-state/tenant-lease.js +150 -0
- package/dist/src/tenant-state/tenant-router.js +650 -0
- package/extension/manifest.json +2 -2
- package/package.json +5 -1
- package/ts/capabilities/flyai-config.ts +12 -10
- package/ts/capabilities/flyai.ts +91 -50
- package/ts/capabilities/hbcli-config.ts +128 -0
- package/ts/capabilities/hbcli.ts +30 -3
- package/ts/capabilities/session/adapters/ctrip-flight.ts +43 -7
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +51 -4
- package/ts/capabilities/session/bridge-ledger.ts +192 -78
- package/ts/capabilities/session/ctrip-city.ts +158 -0
- package/ts/capabilities/session/extension-bridge.ts +185 -67
- package/ts/capabilities/session/gui-flight.ts +1022 -0
- package/ts/capabilities/session/gui-hotel.ts +505 -0
- package/ts/capabilities/session-search.ts +130 -18
- package/ts/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
- package/ts/src/booking-surface/dsh-planner.ts +169 -19
- package/ts/src/booking-surface/index.ts +1 -0
- package/ts/src/booking-surface/managed-dsh-run-port.ts +150 -7
- package/ts/src/booking-surface/server.ts +215 -84
- package/ts/src/booking-surface/startup.ts +155 -0
- package/ts/src/booking-surface/tenancy.ts +242 -0
- package/ts/src/gotry-web-api.ts +51 -2
- package/ts/src/index.ts +67 -19
- package/ts/src/llm-price-table.ts +180 -0
- package/ts/src/tenant-seam/index.ts +9 -0
- package/ts/src/tenant-seam/llm-broker.ts +520 -0
- package/ts/src/tenant-seam/process-governor.ts +343 -0
- package/ts/src/tenant-seam/quotas.ts +433 -0
- package/ts/src/tenant-seam/seam.ts +142 -0
- package/ts/src/tenant-state/durable-store.ts +171 -0
- package/ts/src/tenant-state/epoch-copies.ts +491 -0
- package/ts/src/tenant-state/errors.ts +128 -0
- package/ts/src/tenant-state/ids.ts +108 -0
- package/ts/src/tenant-state/index.ts +12 -0
- package/ts/src/tenant-state/lease-store.ts +266 -0
- package/ts/src/tenant-state/tenant-lease.ts +175 -0
- package/ts/src/tenant-state/tenant-router.ts +743 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The holder side of a tenant lease (docs/rfc/aaas-rfc.md §4.2, items 2, 4 and 6).
|
|
3
|
+
*
|
|
4
|
+
* A replica owns a tenant between a successful `acquire` and the first of: an
|
|
5
|
+
* explicit `release`, a refused `renew`, or the local fence deadline. The deadline
|
|
6
|
+
* is measured on the replica's monotonic clock from BEFORE the request was sent,
|
|
7
|
+
* so the round trip, the store's clock and any skew between the two clocks only
|
|
8
|
+
* make the holder stop earlier:
|
|
9
|
+
*
|
|
10
|
+
* fence = sentAt + grantedTtl - margin
|
|
11
|
+
*
|
|
12
|
+
* `assertHeld()` is the synchronous gate of the write path. A write is
|
|
13
|
+
* acknowledged only if the gate passes before it and after it.
|
|
14
|
+
*
|
|
15
|
+
* What the local fence cannot see is a frozen clock: a process paused between its
|
|
16
|
+
* check and its acknowledgement. That case is covered by the durable layer, whose
|
|
17
|
+
* copies are scoped by epoch, not by this class.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { performance } from 'node:perf_hooks'
|
|
21
|
+
import { FencedError, LeaseLostError, type FenceReason } from './errors.ts'
|
|
22
|
+
import { assertLeaseTtl, type LeaseRecord, type LeaseStore } from './lease-store.ts'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A monotonic clock owned by the replica. Production uses `performance.now()`, which
|
|
26
|
+
* does not jump with wall-clock changes. It stops while a host is suspended, which
|
|
27
|
+
* is the frozen-clock case above.
|
|
28
|
+
*/
|
|
29
|
+
export interface MonotonicClock {
|
|
30
|
+
nowMs(): number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const systemMonotonicClock: MonotonicClock = { nowMs: () => performance.now() }
|
|
34
|
+
|
|
35
|
+
export interface TenantLeaseOptions {
|
|
36
|
+
readonly store: LeaseStore
|
|
37
|
+
readonly key: string
|
|
38
|
+
/** Unique per process incarnation: a restarted replica is a different holder and waits for expiry or release. */
|
|
39
|
+
readonly replicaId: string
|
|
40
|
+
readonly ttlMs: number
|
|
41
|
+
/** The agreed clock-skew and scheduling bound; the holder stops this long before the store would admit anyone else. */
|
|
42
|
+
readonly marginMs: number
|
|
43
|
+
readonly clock?: MonotonicClock
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type HolderState = 'held' | 'released' | 'lost'
|
|
47
|
+
|
|
48
|
+
export class TenantLease {
|
|
49
|
+
readonly key: string
|
|
50
|
+
readonly replicaId: string
|
|
51
|
+
readonly epoch: number
|
|
52
|
+
/** Durable copies of epochs up to this one are dead; fixed for the whole tenure, because only the holder can raise it. */
|
|
53
|
+
readonly forgottenAtEpoch: number
|
|
54
|
+
private readonly store: LeaseStore
|
|
55
|
+
private readonly clock: MonotonicClock
|
|
56
|
+
private readonly ttlMs: number
|
|
57
|
+
private readonly marginMs: number
|
|
58
|
+
private fenceAtMs: number
|
|
59
|
+
private state: HolderState = 'held'
|
|
60
|
+
private tail: Promise<void> = Promise.resolve()
|
|
61
|
+
|
|
62
|
+
private constructor(options: TenantLeaseOptions, clock: MonotonicClock, record: LeaseRecord, fenceAtMs: number) {
|
|
63
|
+
this.key = options.key
|
|
64
|
+
this.replicaId = options.replicaId
|
|
65
|
+
this.epoch = record.epoch
|
|
66
|
+
this.forgottenAtEpoch = record.forgottenAtEpoch
|
|
67
|
+
this.store = options.store
|
|
68
|
+
this.clock = clock
|
|
69
|
+
this.ttlMs = options.ttlMs
|
|
70
|
+
this.marginMs = options.marginMs
|
|
71
|
+
this.fenceAtMs = fenceAtMs
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @throws LeaseHeldError when another holder has an unexpired lease
|
|
76
|
+
* @throws LeaseStoreUnavailableError when the store cannot be reached
|
|
77
|
+
*/
|
|
78
|
+
static async acquire(options: TenantLeaseOptions): Promise<TenantLease> {
|
|
79
|
+
assertLeaseTtl(options.ttlMs)
|
|
80
|
+
if (!Number.isInteger(options.marginMs) || options.marginMs < 0 || options.marginMs >= options.ttlMs) {
|
|
81
|
+
throw new RangeError(`lease margin must be an integer within [0, ttl), got ${String(options.marginMs)} for ttl ${options.ttlMs}`)
|
|
82
|
+
}
|
|
83
|
+
const clock = options.clock ?? systemMonotonicClock
|
|
84
|
+
const sentAt = clock.nowMs()
|
|
85
|
+
const grant = await options.store.acquire(options.key, options.replicaId, options.ttlMs)
|
|
86
|
+
if (grant.record.owner !== options.replicaId || grant.record.key !== options.key) {
|
|
87
|
+
throw new LeaseLostError(`${options.key}: the store granted the lease to ${grant.record.owner}`)
|
|
88
|
+
}
|
|
89
|
+
const lease = new TenantLease(options, clock, grant.record, sentAt + grant.ttlMs - options.marginMs)
|
|
90
|
+
if (lease.fenceAtMs <= clock.nowMs()) {
|
|
91
|
+
// The round trip consumed the usable part of the lease; hand it back instead of holding an already fenced epoch.
|
|
92
|
+
await lease.release().catch(() => undefined)
|
|
93
|
+
throw new LeaseLostError(`${options.key}: the grant of epoch ${grant.record.epoch} was consumed by the round trip`)
|
|
94
|
+
}
|
|
95
|
+
return lease
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The synchronous gate of the write path. */
|
|
99
|
+
assertHeld(): void {
|
|
100
|
+
if (this.state === 'released') throw this.fenced('lease_released')
|
|
101
|
+
if (this.state === 'lost') throw this.fenced('lease_lost')
|
|
102
|
+
if (this.clock.nowMs() >= this.fenceAtMs) throw this.fenced('lease_expired_locally')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
isHeld(): boolean {
|
|
106
|
+
try {
|
|
107
|
+
this.assertHeld()
|
|
108
|
+
return true
|
|
109
|
+
} catch {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Milliseconds until the local fence; zero or negative once fenced. */
|
|
115
|
+
remainingMs(): number {
|
|
116
|
+
return this.fenceAtMs - this.clock.nowMs()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Extends the lease; the epoch stays. A refusal is terminal for this holder. Any other
|
|
121
|
+
* failure leaves the deadline untouched: the holder fails closed when it passes, and a
|
|
122
|
+
* later successful renewal resumes it, because an unchanged epoch means nobody else held it.
|
|
123
|
+
*/
|
|
124
|
+
renew(): Promise<void> {
|
|
125
|
+
return this.serialize(async () => {
|
|
126
|
+
if (this.state !== 'held') throw this.fenced(this.state === 'released' ? 'lease_released' : 'lease_lost')
|
|
127
|
+
const sentAt = this.clock.nowMs()
|
|
128
|
+
try {
|
|
129
|
+
const grant = await this.store.renew(this.key, this.replicaId, this.epoch, this.ttlMs)
|
|
130
|
+
if (grant.record.epoch !== this.epoch || grant.record.owner !== this.replicaId) {
|
|
131
|
+
throw new LeaseLostError(`${this.key}: renewal answered for epoch ${grant.record.epoch}`)
|
|
132
|
+
}
|
|
133
|
+
this.fenceAtMs = Math.max(this.fenceAtMs, sentAt + grant.ttlMs - this.marginMs)
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof LeaseLostError) this.state = 'lost'
|
|
136
|
+
throw error
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Graceful handoff step: stop acknowledging first, then tell the store, so the next owner
|
|
143
|
+
* can start at once. If the store cannot be reached the lease simply expires; the error is
|
|
144
|
+
* rethrown so the caller knows the handoff degraded to waiting out the TTL.
|
|
145
|
+
*/
|
|
146
|
+
release(): Promise<void> {
|
|
147
|
+
return this.serialize(async () => {
|
|
148
|
+
if (this.state !== 'held') return
|
|
149
|
+
this.state = 'released'
|
|
150
|
+
try {
|
|
151
|
+
await this.store.release(this.key, this.replicaId, this.epoch)
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof LeaseLostError) return
|
|
154
|
+
throw error
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Marks every durable copy up to this epoch dead (forget). Requires a lease that is provably held. */
|
|
160
|
+
async markForgotten(): Promise<LeaseRecord> {
|
|
161
|
+
this.assertHeld()
|
|
162
|
+
return this.store.markForgotten(this.key, this.replicaId, this.epoch)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private fenced(reason: FenceReason): FencedError {
|
|
166
|
+
return new FencedError(reason, this.key, this.epoch)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Renewals and the release of one holder run one at a time, in call order. */
|
|
170
|
+
private serialize<T>(step: () => Promise<T>): Promise<T> {
|
|
171
|
+
const run = this.tail.then(step, step)
|
|
172
|
+
this.tail = run.then(() => undefined, () => undefined)
|
|
173
|
+
return run
|
|
174
|
+
}
|
|
175
|
+
}
|