@namzu/sdk 20.4.0 → 21.1.0
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/CHANGELOG.md +223 -0
- package/dist/bridge/sse/mapper.d.ts +19 -0
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +9 -1
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/contracts/schemas.d.ts +10 -10
- package/dist/manager/run/persistence.d.ts +26 -0
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +40 -0
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/public-runtime.d.ts +8 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +31 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/run/command-gate.d.ts +107 -0
- package/dist/run/command-gate.d.ts.map +1 -0
- package/dist/run/command-gate.js +157 -0
- package/dist/run/command-gate.js.map +1 -0
- package/dist/run/drain.d.ts +194 -0
- package/dist/run/drain.d.ts.map +1 -0
- package/dist/run/drain.js +228 -0
- package/dist/run/drain.js.map +1 -0
- package/dist/run/index.d.ts +8 -0
- package/dist/run/index.d.ts.map +1 -1
- package/dist/run/index.js +4 -0
- package/dist/run/index.js.map +1 -1
- package/dist/run/memory-promoter.d.ts +70 -0
- package/dist/run/memory-promoter.d.ts.map +1 -0
- package/dist/run/memory-promoter.js +117 -0
- package/dist/run/memory-promoter.js.map +1 -0
- package/dist/run/workspace-fingerprint.d.ts +105 -0
- package/dist/run/workspace-fingerprint.d.ts.map +1 -0
- package/dist/run/workspace-fingerprint.js +147 -0
- package/dist/run/workspace-fingerprint.js.map +1 -0
- package/dist/runtime/query/events.d.ts +12 -0
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +61 -3
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/index.d.ts +36 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +32 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/resume-run.d.ts +25 -0
- package/dist/runtime/query/resume-run.d.ts.map +1 -1
- package/dist/runtime/query/resume-run.js +15 -3
- package/dist/runtime/query/resume-run.js.map +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -1
- package/dist/store/index.js.map +1 -1
- package/dist/store/run/conformance.d.ts +159 -0
- package/dist/store/run/conformance.d.ts.map +1 -0
- package/dist/store/run/conformance.js +451 -0
- package/dist/store/run/conformance.js.map +1 -0
- package/dist/store/run/disk.d.ts +30 -2
- package/dist/store/run/disk.d.ts.map +1 -1
- package/dist/store/run/disk.js +101 -0
- package/dist/store/run/disk.js.map +1 -1
- package/dist/store/run/memory.d.ts +5 -4
- package/dist/store/run/memory.d.ts.map +1 -1
- package/dist/store/run/memory.js +30 -1
- package/dist/store/run/memory.js.map +1 -1
- package/dist/types/doctor/check.d.ts +28 -2
- package/dist/types/doctor/check.d.ts.map +1 -1
- package/dist/types/run/event-cursor.d.ts +90 -0
- package/dist/types/run/event-cursor.d.ts.map +1 -0
- package/dist/types/run/event-cursor.js +47 -0
- package/dist/types/run/event-cursor.js.map +1 -0
- package/dist/types/run/events.d.ts +71 -1
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/run/index.d.ts +1 -0
- package/dist/types/run/index.d.ts.map +1 -1
- package/dist/types/run/index.js +1 -0
- package/dist/types/run/index.js.map +1 -1
- package/dist/types/run/store.d.ts +41 -1
- package/dist/types/run/store.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/bridge/sse/mapper.ts +28 -1
- package/src/manager/run/persistence.ts +43 -0
- package/src/public-runtime.ts +53 -0
- package/src/run/command-gate.ts +234 -0
- package/src/run/drain.ts +393 -0
- package/src/run/index.ts +20 -0
- package/src/run/memory-promoter.ts +155 -0
- package/src/run/workspace-fingerprint.ts +193 -0
- package/src/runtime/query/events.ts +68 -4
- package/src/runtime/query/index.ts +87 -0
- package/src/runtime/query/resume-run.ts +67 -12
- package/src/store/index.ts +1 -1
- package/src/store/run/conformance.ts +705 -0
- package/src/store/run/disk.ts +106 -2
- package/src/store/run/memory.ts +35 -5
- package/src/types/doctor/check.ts +28 -2
- package/src/types/run/event-cursor.ts +118 -0
- package/src/types/run/events.ts +80 -3
- package/src/types/run/index.ts +1 -0
- package/src/types/run/store.ts +43 -1
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The checkpoint-store contract, as a suite a host can run against its own
|
|
3
|
+
* backend.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this ships rather than staying a test
|
|
6
|
+
*
|
|
7
|
+
* {@link import('./checkpoint-memory.js').InMemoryCheckpointStore} says in its
|
|
8
|
+
* own doc comment that it is "the reference a host reads when writing a
|
|
9
|
+
* backend of its own". That claim was unbacked, and the cost of it was not
|
|
10
|
+
* hypothetical: the two shipped stores disagreed at the enforcement point —
|
|
11
|
+
* the in-memory one accepted a write from a holder that had been superseded
|
|
12
|
+
* and then released around, the disk one refused it — and the class documented
|
|
13
|
+
* as the reference was the one carrying the defect. Reading a reference cannot
|
|
14
|
+
* tell you that. Running it can.
|
|
15
|
+
*
|
|
16
|
+
* The rules below are the ones a claim depends on and no type can state:
|
|
17
|
+
* exclusivity, expiry, that a superseded write is refused, and that a listing
|
|
18
|
+
* answers for the tenant it was asked about and no other. Those are exactly
|
|
19
|
+
* the points at which the two built-in implementations already diverged.
|
|
20
|
+
*
|
|
21
|
+
* ## Why it takes its runner as an argument
|
|
22
|
+
*
|
|
23
|
+
* `describe`, `it` and `expect` come in through
|
|
24
|
+
* {@link CheckpointStoreConformanceOptions}. The suite therefore imports no
|
|
25
|
+
* test framework, `@namzu/sdk` gains no test dependency from publishing it,
|
|
26
|
+
* and a host on a different runner can still run it by handing over three
|
|
27
|
+
* functions of the shapes below.
|
|
28
|
+
*
|
|
29
|
+
* It also buys the one property that separates this file from decoration: a
|
|
30
|
+
* caller can pass a *recording* `describe`/`it` and run the whole contract as
|
|
31
|
+
* ordinary code, which is how `conformance-fails-a-wrong-store.test.ts`
|
|
32
|
+
* establishes that a deliberately broken store fails it.
|
|
33
|
+
*
|
|
34
|
+
* ## Consuming it
|
|
35
|
+
*
|
|
36
|
+
* See `docs/sdk/runtime/checkpoint-store-conformance.md`. In short:
|
|
37
|
+
*
|
|
38
|
+
* ```typescript
|
|
39
|
+
* import { describe, expect, it } from 'your-runner'
|
|
40
|
+
* import { defineCheckpointStoreConformance } from '@namzu/sdk/testing'
|
|
41
|
+
*
|
|
42
|
+
* defineCheckpointStoreConformance({
|
|
43
|
+
* describe, it, expect,
|
|
44
|
+
* label: 'my-backend',
|
|
45
|
+
* contractVersion: 1,
|
|
46
|
+
* capabilities: { claims: true, listing: true, multiTenant: true },
|
|
47
|
+
* makeStore: (binding) => ({ store: new MyStore(binding) }),
|
|
48
|
+
* })
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* ## The assertions are public API
|
|
52
|
+
*
|
|
53
|
+
* Once a host wires this in, every assertion below is something their build
|
|
54
|
+
* fails on. Adding one is therefore a breaking change for them even though it
|
|
55
|
+
* adds no export — so a new rule arrives behind a raised
|
|
56
|
+
* {@link CHECKPOINT_STORE_CONTRACT_VERSION} and a `major`, and the version
|
|
57
|
+
* check is what turns "your backend silently satisfies an older, smaller
|
|
58
|
+
* contract" into a named failure.
|
|
59
|
+
*/
|
|
60
|
+
import { claimRun, listDurableRuns, releaseRun } from './listing.js';
|
|
61
|
+
/**
|
|
62
|
+
* Which revision of the contract the assertions in this file express.
|
|
63
|
+
*
|
|
64
|
+
* A host DECLARES the version it wrote its backend against, as a literal in
|
|
65
|
+
* its own test file, and the suite's first case compares the two. Re-exporting
|
|
66
|
+
* this constant into that slot defeats the check — the point is that the
|
|
67
|
+
* number is frozen in the host's source at the moment they wrote the backend,
|
|
68
|
+
* so upgrading `@namzu/sdk` past a contract revision fails with a sentence
|
|
69
|
+
* naming both numbers instead of a scatter of assertion failures whose common
|
|
70
|
+
* cause is not obvious.
|
|
71
|
+
*
|
|
72
|
+
* Raised only together with a `major`, and only when an assertion is ADDED or
|
|
73
|
+
* TIGHTENED. Adding a case behind a new optional capability flag does not
|
|
74
|
+
* raise it: a host that does not declare the capability never runs the case.
|
|
75
|
+
*/
|
|
76
|
+
export const CHECKPOINT_STORE_CONTRACT_VERSION = 1;
|
|
77
|
+
/** The tenant/project/session every case addresses unless it needs a second. */
|
|
78
|
+
const BINDING = {
|
|
79
|
+
tenantId: 'tnt_conformance',
|
|
80
|
+
projectId: 'prj_conformance',
|
|
81
|
+
sessionId: 'ses_conformance',
|
|
82
|
+
};
|
|
83
|
+
/** A tenant no backend under test holds runs for. */
|
|
84
|
+
const FOREIGN_TENANT = 'tnt_conformance_other';
|
|
85
|
+
/** Fixed instant, so every expiry in a case is judged against one clock. */
|
|
86
|
+
const NOW = 5_000_000;
|
|
87
|
+
const NO_COST = {
|
|
88
|
+
inputCostPer1M: 0,
|
|
89
|
+
outputCostPer1M: 0,
|
|
90
|
+
totalCost: 0,
|
|
91
|
+
cacheDiscount: 0,
|
|
92
|
+
};
|
|
93
|
+
let checkpointSeq = 0;
|
|
94
|
+
function checkpoint(runId) {
|
|
95
|
+
checkpointSeq += 1;
|
|
96
|
+
return {
|
|
97
|
+
id: `cp_conformance_${checkpointSeq}`,
|
|
98
|
+
runId: runId,
|
|
99
|
+
iteration: 1,
|
|
100
|
+
messages: [],
|
|
101
|
+
tokenUsage: {
|
|
102
|
+
promptTokens: 1,
|
|
103
|
+
completionTokens: 1,
|
|
104
|
+
totalTokens: 2,
|
|
105
|
+
cachedTokens: 0,
|
|
106
|
+
cacheWriteTokens: 0,
|
|
107
|
+
},
|
|
108
|
+
costInfo: NO_COST,
|
|
109
|
+
guardState: { iterationCount: 1, elapsedMs: 1 },
|
|
110
|
+
createdAt: NOW,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function runScope(binding, runId = 'run_conformance_a') {
|
|
114
|
+
return { ...binding, runId: runId };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Assert that `call` rejected with a message matching `pattern`.
|
|
118
|
+
*
|
|
119
|
+
* Written against `toMatch` rather than a runner's `rejects` helper so the
|
|
120
|
+
* suite needs no matcher beyond the four on {@link ConformanceAssertion}. A
|
|
121
|
+
* call that RESOLVES yields a sentence saying so, which is the diagnostic that
|
|
122
|
+
* matters: the failure this whole file exists for is a write that was accepted
|
|
123
|
+
* when it should have been refused.
|
|
124
|
+
*/
|
|
125
|
+
async function expectRejection(expect, call, pattern) {
|
|
126
|
+
let message = '<the call resolved; no rejection was raised>';
|
|
127
|
+
try {
|
|
128
|
+
await call();
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
}
|
|
133
|
+
expect(message).toMatch(pattern);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Register the checkpoint-store contract against one backend.
|
|
137
|
+
*
|
|
138
|
+
* Call it once per backend. It registers cases through the supplied `describe`
|
|
139
|
+
* and `it`; it does not run them.
|
|
140
|
+
*/
|
|
141
|
+
export function defineCheckpointStoreConformance(options) {
|
|
142
|
+
const { describe, it, expect, capabilities, makeStore } = options;
|
|
143
|
+
const label = options.label ?? 'checkpoint store';
|
|
144
|
+
/**
|
|
145
|
+
* Run `body` against a store built for this case alone.
|
|
146
|
+
*
|
|
147
|
+
* `finally`, so a failing assertion still disposes — a suite that leaks a
|
|
148
|
+
* temp directory per failure makes a red run expensive to iterate on, and
|
|
149
|
+
* an expensive red run is one people stop running.
|
|
150
|
+
*/
|
|
151
|
+
const withStore = (body) => async () => {
|
|
152
|
+
const handle = await makeStore(BINDING);
|
|
153
|
+
try {
|
|
154
|
+
await body(handle.store);
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
await handle.dispose?.();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
describe(`${label} conformance`, () => {
|
|
161
|
+
it('declares the contract revision this suite implements', async () => {
|
|
162
|
+
// First case on purpose. When it fails, every case below it is
|
|
163
|
+
// answering a different contract than the backend was written for,
|
|
164
|
+
// and reading their failures as defects would send a host chasing
|
|
165
|
+
// bugs that are really a version skew.
|
|
166
|
+
//
|
|
167
|
+
// Both numbers ride in the COMPARED VALUES rather than in a
|
|
168
|
+
// sentence, because a runner truncates a long actual when it builds
|
|
169
|
+
// the failure message — measured, on the first draft of this, at 37
|
|
170
|
+
// characters, which cut off the very numbers the case exists to
|
|
171
|
+
// report. The guidance that would not fit lives on
|
|
172
|
+
// `CHECKPOINT_STORE_CONTRACT_VERSION`, which is where a reader of
|
|
173
|
+
// this failure is being sent anyway.
|
|
174
|
+
expect(`checkpoint-store contract v${options.contractVersion}`).toBe(`checkpoint-store contract v${CHECKPOINT_STORE_CONTRACT_VERSION}`);
|
|
175
|
+
});
|
|
176
|
+
if (capabilities.claims) {
|
|
177
|
+
describe('claim exclusivity', () => {
|
|
178
|
+
it('gives the run to the first taker and refuses the second', withStore(async (store) => {
|
|
179
|
+
const first = await claimRun(store, runScope(BINDING), {
|
|
180
|
+
holder: 'w1',
|
|
181
|
+
ttlMs: 60_000,
|
|
182
|
+
now: NOW,
|
|
183
|
+
});
|
|
184
|
+
const second = await claimRun(store, runScope(BINDING), {
|
|
185
|
+
holder: 'w2',
|
|
186
|
+
ttlMs: 60_000,
|
|
187
|
+
now: NOW,
|
|
188
|
+
});
|
|
189
|
+
expect(first?.holder).toBe('w1');
|
|
190
|
+
// `null`, not a throw: two readers on one queue is the
|
|
191
|
+
// ordinary case, and an exception would make the normal
|
|
192
|
+
// outcome look like a fault.
|
|
193
|
+
expect(second).toBe(null);
|
|
194
|
+
}));
|
|
195
|
+
it('advances the fence on renewal, so a stalled twin cannot write', withStore(async (store) => {
|
|
196
|
+
const first = await claimRun(store, runScope(BINDING), {
|
|
197
|
+
holder: 'w1',
|
|
198
|
+
ttlMs: 1_000,
|
|
199
|
+
now: NOW,
|
|
200
|
+
});
|
|
201
|
+
const renewed = await claimRun(store, runScope(BINDING), {
|
|
202
|
+
holder: 'w1',
|
|
203
|
+
ttlMs: 60_000,
|
|
204
|
+
now: NOW + 500,
|
|
205
|
+
});
|
|
206
|
+
// Renewal and reclamation are one operation and both
|
|
207
|
+
// advance. A renewal that kept the fence would leave any
|
|
208
|
+
// duplicate of the holder — a retried job, a
|
|
209
|
+
// double-scheduled pod — able to write with the number it
|
|
210
|
+
// captured before.
|
|
211
|
+
expect(renewed?.fence).toBeGreaterThan(first?.fence);
|
|
212
|
+
}));
|
|
213
|
+
it('refuses a lease with no duration', withStore(async (store) => {
|
|
214
|
+
// A lease that expires immediately is a lease every worker
|
|
215
|
+
// can take at once, which is the condition the call exists
|
|
216
|
+
// to prevent.
|
|
217
|
+
await expectRejection(expect, () => claimRun(store, runScope(BINDING), { holder: 'w1', ttlMs: 0, now: NOW }), /positive number of milliseconds/);
|
|
218
|
+
}));
|
|
219
|
+
});
|
|
220
|
+
describe('claim expiry', () => {
|
|
221
|
+
it('lets a later worker take a run whose holder went away', withStore(async (store) => {
|
|
222
|
+
const first = await claimRun(store, runScope(BINDING), {
|
|
223
|
+
holder: 'w1',
|
|
224
|
+
ttlMs: 1_000,
|
|
225
|
+
now: NOW,
|
|
226
|
+
});
|
|
227
|
+
const second = await claimRun(store, runScope(BINDING), {
|
|
228
|
+
holder: 'w2',
|
|
229
|
+
ttlMs: 60_000,
|
|
230
|
+
now: NOW + 2_000,
|
|
231
|
+
});
|
|
232
|
+
// A lock held by a dead process is held forever. The expiry
|
|
233
|
+
// is the whole difference between a lease and a wedged run.
|
|
234
|
+
expect(second?.holder).toBe('w2');
|
|
235
|
+
expect(second?.fence).toBeGreaterThan(first?.fence);
|
|
236
|
+
}));
|
|
237
|
+
it('returns the run to the queue when its holder releases', withStore(async (store) => {
|
|
238
|
+
const claim = await claimRun(store, runScope(BINDING), {
|
|
239
|
+
holder: 'w1',
|
|
240
|
+
ttlMs: 60_000,
|
|
241
|
+
now: NOW,
|
|
242
|
+
});
|
|
243
|
+
await releaseRun(store, runScope(BINDING), claim?.fence);
|
|
244
|
+
const next = await claimRun(store, runScope(BINDING), {
|
|
245
|
+
holder: 'w2',
|
|
246
|
+
ttlMs: 60_000,
|
|
247
|
+
now: NOW + 1,
|
|
248
|
+
});
|
|
249
|
+
expect(next?.holder).toBe('w2');
|
|
250
|
+
}));
|
|
251
|
+
it('releases only on the fence that currently holds it', withStore(async (store) => {
|
|
252
|
+
const first = await claimRun(store, runScope(BINDING), {
|
|
253
|
+
holder: 'w1',
|
|
254
|
+
ttlMs: 1_000,
|
|
255
|
+
now: NOW,
|
|
256
|
+
});
|
|
257
|
+
await claimRun(store, runScope(BINDING), {
|
|
258
|
+
holder: 'w2',
|
|
259
|
+
ttlMs: 60_000,
|
|
260
|
+
now: NOW + 2_000,
|
|
261
|
+
});
|
|
262
|
+
// A worker that stalled past its lease must not be able to
|
|
263
|
+
// hand away a run somebody else is now holding.
|
|
264
|
+
await releaseRun(store, runScope(BINDING), first?.fence);
|
|
265
|
+
const third = await claimRun(store, runScope(BINDING), {
|
|
266
|
+
holder: 'w3',
|
|
267
|
+
ttlMs: 60_000,
|
|
268
|
+
now: NOW + 3_000,
|
|
269
|
+
});
|
|
270
|
+
expect(third).toBe(null);
|
|
271
|
+
}));
|
|
272
|
+
});
|
|
273
|
+
describe('fenced-out writes', () => {
|
|
274
|
+
it('lets the current holder keep writing', withStore(async (store) => {
|
|
275
|
+
const claim = await claimRun(store, runScope(BINDING), {
|
|
276
|
+
holder: 'w1',
|
|
277
|
+
ttlMs: 60_000,
|
|
278
|
+
now: NOW,
|
|
279
|
+
});
|
|
280
|
+
await store.writeCheckpoint(runScope(BINDING), checkpoint('run_conformance_a'), claim?.fence);
|
|
281
|
+
const written = await store.listCheckpoints(runScope(BINDING));
|
|
282
|
+
expect(written.length).toBe(1);
|
|
283
|
+
}));
|
|
284
|
+
it('still accepts an unfenced write on a claimed run', withStore(async (store) => {
|
|
285
|
+
await claimRun(store, runScope(BINDING), {
|
|
286
|
+
holder: 'w1',
|
|
287
|
+
ttlMs: 60_000,
|
|
288
|
+
now: NOW,
|
|
289
|
+
});
|
|
290
|
+
// A host that adopts claims on one worker must not break the
|
|
291
|
+
// workers that have not adopted them yet. Refusing here would
|
|
292
|
+
// make the capability impossible to roll out incrementally.
|
|
293
|
+
await store.writeCheckpoint(runScope(BINDING), checkpoint('run_conformance_a'));
|
|
294
|
+
const written = await store.listCheckpoints(runScope(BINDING));
|
|
295
|
+
expect(written.length).toBe(1);
|
|
296
|
+
}));
|
|
297
|
+
it('fences the stalled holder out at the moment it writes', withStore(async (store) => {
|
|
298
|
+
const first = await claimRun(store, runScope(BINDING), {
|
|
299
|
+
holder: 'w1',
|
|
300
|
+
ttlMs: 1_000,
|
|
301
|
+
now: NOW,
|
|
302
|
+
});
|
|
303
|
+
await claimRun(store, runScope(BINDING), {
|
|
304
|
+
holder: 'w2',
|
|
305
|
+
ttlMs: 60_000,
|
|
306
|
+
now: NOW + 2_000,
|
|
307
|
+
});
|
|
308
|
+
// `w1` believes it still holds the run. It cannot know
|
|
309
|
+
// otherwise: a long pause, a suspended container and a
|
|
310
|
+
// partition all look from the inside like time not passing.
|
|
311
|
+
// The write is the only place it can be told, and this is
|
|
312
|
+
// that place.
|
|
313
|
+
await expectRejection(expect, () => store.writeCheckpoint(runScope(BINDING), checkpoint('run_conformance_a'), first?.fence), /no longer holds it/);
|
|
314
|
+
}));
|
|
315
|
+
it('still refuses a superseded fence after the new holder releases', withStore(async (store) => {
|
|
316
|
+
// The silent one, and the case on which the two built-in
|
|
317
|
+
// stores actually disagreed. Not a duplicate write — a LOST
|
|
318
|
+
// one.
|
|
319
|
+
//
|
|
320
|
+
// w1 stalls at its fence. w2 reclaims, does the work, and
|
|
321
|
+
// releases cleanly, which is the documented
|
|
322
|
+
// `finally { releaseRun() }`. w1 then wakes believing it
|
|
323
|
+
// still holds the run and writes. If that write is accepted,
|
|
324
|
+
// its checkpoint carries a fresh `createdAt`, sorts newest,
|
|
325
|
+
// and the next resume restores w1's stale history — so w2's
|
|
326
|
+
// completed work vanishes with no error anywhere.
|
|
327
|
+
const stale = await claimRun(store, runScope(BINDING), {
|
|
328
|
+
holder: 'w1',
|
|
329
|
+
ttlMs: 1_000,
|
|
330
|
+
now: NOW,
|
|
331
|
+
});
|
|
332
|
+
const live = await claimRun(store, runScope(BINDING), {
|
|
333
|
+
holder: 'w2',
|
|
334
|
+
ttlMs: 60_000,
|
|
335
|
+
now: NOW + 2_000,
|
|
336
|
+
});
|
|
337
|
+
await releaseRun(store, runScope(BINDING), live?.fence);
|
|
338
|
+
await expectRejection(expect, () => store.writeCheckpoint(runScope(BINDING), checkpoint('run_conformance_a'), stale?.fence), /no longer holds it/);
|
|
339
|
+
}));
|
|
340
|
+
it('refuses the fence its own holder just released', withStore(async (store) => {
|
|
341
|
+
// The same hole one step shorter, and the one that pins the
|
|
342
|
+
// release itself rather than a reclaim before it. A holder
|
|
343
|
+
// that gives a run back has given up the right to write to
|
|
344
|
+
// it; nothing else took the run in between, so only the
|
|
345
|
+
// release can be what refuses this.
|
|
346
|
+
const claim = await claimRun(store, runScope(BINDING), {
|
|
347
|
+
holder: 'w1',
|
|
348
|
+
ttlMs: 60_000,
|
|
349
|
+
now: NOW,
|
|
350
|
+
});
|
|
351
|
+
await releaseRun(store, runScope(BINDING), claim?.fence);
|
|
352
|
+
await expectRejection(expect, () => store.writeCheckpoint(runScope(BINDING), checkpoint('run_conformance_a'), claim?.fence), /no longer holds it/);
|
|
353
|
+
}));
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
if (capabilities.listing) {
|
|
357
|
+
describe('listing scope isolation', () => {
|
|
358
|
+
it('answers nothing for a tenant it does not hold', withStore(async (store) => {
|
|
359
|
+
await store.writeCheckpoint(runScope(BINDING, 'run_conformance_a'), checkpoint('run_conformance_a'));
|
|
360
|
+
const foreign = await listDurableRuns(store, { tenantId: FOREIGN_TENANT });
|
|
361
|
+
// A listing is SCOPED, not addressed: another tenant is a
|
|
362
|
+
// question this store has no rows for. Leaking one row here
|
|
363
|
+
// is the whole of a cross-tenant read.
|
|
364
|
+
expect(foreign.entries.map((e) => e.runId)).toEqual([]);
|
|
365
|
+
}));
|
|
366
|
+
it('answers nothing for a project it does not hold', withStore(async (store) => {
|
|
367
|
+
await store.writeCheckpoint(runScope(BINDING, 'run_conformance_a'), checkpoint('run_conformance_a'));
|
|
368
|
+
const foreign = await listDurableRuns(store, {
|
|
369
|
+
tenantId: BINDING.tenantId,
|
|
370
|
+
projectId: 'prj_conformance_other',
|
|
371
|
+
});
|
|
372
|
+
expect(foreign.entries.map((e) => e.runId)).toEqual([]);
|
|
373
|
+
}));
|
|
374
|
+
it('refuses a listing scope with a hole in it', withStore(async (store) => {
|
|
375
|
+
// `{ tenantId, sessionId }` reads as "that session under
|
|
376
|
+
// whichever project holds it". A flat backend can answer it
|
|
377
|
+
// and a hierarchical one cannot, so answering differently per
|
|
378
|
+
// backend is the one thing this contract exists to prevent.
|
|
379
|
+
await expectRejection(expect, () => listDurableRuns(store, {
|
|
380
|
+
tenantId: BINDING.tenantId,
|
|
381
|
+
sessionId: BINDING.sessionId,
|
|
382
|
+
}), /contiguous prefix|without `projectId`/);
|
|
383
|
+
}));
|
|
384
|
+
it('rebuilds an addressable row for every run it holds', withStore(async (store) => {
|
|
385
|
+
await store.writeCheckpoint(runScope(BINDING, 'run_conformance_a'), checkpoint('run_conformance_a'));
|
|
386
|
+
await store.writeCheckpoint(runScope(BINDING, 'run_conformance_b'), checkpoint('run_conformance_b'));
|
|
387
|
+
const page = await listDurableRuns(store, { tenantId: BINDING.tenantId });
|
|
388
|
+
expect(page.entries.map((e) => e.runId)).toEqual([
|
|
389
|
+
'run_conformance_a',
|
|
390
|
+
'run_conformance_b',
|
|
391
|
+
]);
|
|
392
|
+
// A row that cannot be turned back into a scope is a report,
|
|
393
|
+
// not a work queue.
|
|
394
|
+
expect(page.entries.map((e) => e.tenantId)).toEqual([
|
|
395
|
+
BINDING.tenantId,
|
|
396
|
+
BINDING.tenantId,
|
|
397
|
+
]);
|
|
398
|
+
expect(page.entries.map((e) => e.sessionId)).toEqual([
|
|
399
|
+
BINDING.sessionId,
|
|
400
|
+
BINDING.sessionId,
|
|
401
|
+
]);
|
|
402
|
+
}));
|
|
403
|
+
if (capabilities.multiTenant) {
|
|
404
|
+
it('keeps one tenant’s runs out of another tenant’s listing', withStore(async (store) => {
|
|
405
|
+
await store.writeCheckpoint(runScope(BINDING, 'run_conformance_a'), checkpoint('run_conformance_a'));
|
|
406
|
+
await store.writeCheckpoint({
|
|
407
|
+
tenantId: FOREIGN_TENANT,
|
|
408
|
+
projectId: BINDING.projectId,
|
|
409
|
+
sessionId: BINDING.sessionId,
|
|
410
|
+
runId: 'run_conformance_z',
|
|
411
|
+
}, checkpoint('run_conformance_z'));
|
|
412
|
+
const mine = await listDurableRuns(store, { tenantId: BINDING.tenantId });
|
|
413
|
+
expect(mine.entries.map((e) => e.runId)).toEqual(['run_conformance_a']);
|
|
414
|
+
const theirs = await listDurableRuns(store, { tenantId: FOREIGN_TENANT });
|
|
415
|
+
expect(theirs.entries.map((e) => e.runId)).toEqual(['run_conformance_z']);
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
if (capabilities.claims) {
|
|
419
|
+
it('shows the queue reader which runs nobody holds', withStore(async (store) => {
|
|
420
|
+
for (const id of ['run_conformance_a', 'run_conformance_b', 'run_conformance_c'])
|
|
421
|
+
await store.writeCheckpoint(runScope(BINDING, id), checkpoint(id));
|
|
422
|
+
await claimRun(store, runScope(BINDING, 'run_conformance_a'), {
|
|
423
|
+
holder: 'w1',
|
|
424
|
+
ttlMs: 60_000,
|
|
425
|
+
now: NOW,
|
|
426
|
+
});
|
|
427
|
+
await claimRun(store, runScope(BINDING, 'run_conformance_b'), {
|
|
428
|
+
holder: 'w1',
|
|
429
|
+
ttlMs: 1_000,
|
|
430
|
+
now: NOW,
|
|
431
|
+
});
|
|
432
|
+
const free = await listDurableRuns(store, { tenantId: BINDING.tenantId }, { claimed: false, now: NOW + 2_000 });
|
|
433
|
+
// `run_conformance_b`'s holder is gone. An expired claim
|
|
434
|
+
// counts as unheld, or a dead worker's runs stay invisible
|
|
435
|
+
// forever — the failure the lease exists to prevent,
|
|
436
|
+
// reintroduced by the filter that reads it.
|
|
437
|
+
expect(free.entries.map((e) => e.runId)).toEqual([
|
|
438
|
+
'run_conformance_b',
|
|
439
|
+
'run_conformance_c',
|
|
440
|
+
]);
|
|
441
|
+
const taken = await listDurableRuns(store, { tenantId: BINDING.tenantId }, { claimed: true, now: NOW + 2_000 });
|
|
442
|
+
expect(taken.entries.map((e) => e.runId)).toEqual(['run_conformance_a']);
|
|
443
|
+
expect(taken.entries[0]?.claim?.holder).toBe('w1');
|
|
444
|
+
expect(taken.entries[0]?.claim?.expired).toBe(false);
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
//# sourceMappingURL=conformance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.js","sourceRoot":"","sources":["../../../src/store/run/conformance.ts"],"names":[],"mappings":"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"}
|
package/dist/store/run/disk.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CheckpointId, IterationCheckpoint } from '../../types/hitl/index.js';
|
|
2
|
-
import type { Run, RunEvent, RunStoreConfig } from '../../types/run/index.js';
|
|
3
|
-
import type { CompletedToolRecord, RunStore } from '../../types/run/store.js';
|
|
2
|
+
import type { PersistedRunEvent, Run, RunEvent, RunStoreConfig } from '../../types/run/index.js';
|
|
3
|
+
import type { CompletedToolRecord, ReadRunEventsOptions, RunStore } from '../../types/run/store.js';
|
|
4
4
|
/**
|
|
5
5
|
* One finished tool call, recovered from the transcript.
|
|
6
6
|
*
|
|
@@ -18,6 +18,7 @@ export declare class RunDiskStore implements RunStore {
|
|
|
18
18
|
private requireInit;
|
|
19
19
|
initRun(runId: string, parentRunId?: string): Promise<string>;
|
|
20
20
|
appendEvent(event: RunEvent): Promise<void>;
|
|
21
|
+
readEvents(options?: ReadRunEventsOptions): Promise<readonly PersistedRunEvent[]>;
|
|
21
22
|
/**
|
|
22
23
|
* Every tool call this run has already finished, keyed by `toolUseId`.
|
|
23
24
|
*
|
|
@@ -74,6 +75,33 @@ export declare class RunDiskStore implements RunStore {
|
|
|
74
75
|
}>>;
|
|
75
76
|
addToIndex(run: Run): Promise<void>;
|
|
76
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Every durable event under one run directory, oldest first.
|
|
80
|
+
*
|
|
81
|
+
* A free function for the same reason {@link readCheckpointsIn} is one: a
|
|
82
|
+
* caller catching up on a run this process never started would otherwise have
|
|
83
|
+
* to bind a {@link RunDiskStore} to read it, and binding one CREATES the
|
|
84
|
+
* directory. A read that mints an empty run directory then answers "no events"
|
|
85
|
+
* is indistinguishable from a run that genuinely has none.
|
|
86
|
+
*
|
|
87
|
+
* ## Unsequenced lines take their position
|
|
88
|
+
*
|
|
89
|
+
* A transcript written before events were numbered carries no `seq` at all.
|
|
90
|
+
* Numbering those lines by their 1-based position is what keeps their evidence
|
|
91
|
+
* reachable: a legacy run of five lines reads back as 1..5, seeds the emitter
|
|
92
|
+
* at 5, and its next event is 6 — continuous, and stable on every later read.
|
|
93
|
+
* Skipping them instead would erase a run's whole history from a catch-up, and
|
|
94
|
+
* synthesising nothing at all would put the emitter back at 1 on top of a log
|
|
95
|
+
* that already has five entries.
|
|
96
|
+
*
|
|
97
|
+
* A damaged line is skipped rather than refused, which is the one place this
|
|
98
|
+
* differs from the checkpoint reader next door, and deliberately: a checkpoint
|
|
99
|
+
* is read to RESUME from, so a damaged one must stop the resume, while the
|
|
100
|
+
* transcript is read to REPORT from, and dropping every event after a torn line
|
|
101
|
+
* would be a larger loss than the torn line itself. The position count still
|
|
102
|
+
* advances over it, so the numbering of the events after it is unchanged.
|
|
103
|
+
*/
|
|
104
|
+
export declare function readRunEventsIn(runDir: string, options?: ReadRunEventsOptions): Promise<readonly PersistedRunEvent[]>;
|
|
77
105
|
/**
|
|
78
106
|
* Every checkpoint stored under one run directory, ascending by `createdAt`.
|
|
79
107
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"disk.d.ts","sourceRoot":"","sources":["../../../src/store/run/disk.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAA;AAClF,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"disk.d.ts","sourceRoot":"","sources":["../../../src/store/run/disk.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAA;AAClF,OAAO,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAChG,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAenG;;;;;;GAMG;AACH,YAAY,EAAE,mBAAmB,EAAE,CAAA;AAEnC,qBAAa,YAAa,YAAW,QAAQ;IAC5C,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,GAAG,CAAQ;IACnB,OAAO,CAAC,SAAS,CAAmC;gBAExC,MAAM,EAAE,cAAc;IAKlC,OAAO,CAAC,WAAW;IAOb,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAY7D,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAW3C,UAAU,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC;IAIvF;;;;;;;;;;;;;;OAcG;IACG,kBAAkB,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IA0C/D,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IA2BrC,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAKtC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASnD,SAAS,IAAI,MAAM,GAAG,IAAI;IAIpB,eAAe,CAAC,UAAU,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAc/D,cAAc,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;IAW/E,eAAe,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAIjD,gBAAgB,CAAC,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IASjE;;;;;;;;;;;;;;;;;;;;;;OAsBG;WACU,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAC9C,KAAK,CAAC;QACL,EAAE,EAAE,MAAM,CAAA;QACV,SAAS,EAAE,MAAM,CAAA;QACjB,MAAM,EAAE,MAAM,CAAA;QACd,SAAS,EAAE,MAAM,CAAA;QACjB,OAAO,CAAC,EAAE,MAAM,CAAA;KAChB,CAAC,CACF;IAWK,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;CA8CzC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,eAAe,CACpC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAwCvC;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAiBtF"}
|
package/dist/store/run/disk.js
CHANGED
|
@@ -35,6 +35,7 @@ export class RunDiskStore {
|
|
|
35
35
|
this.runDir = join(this.baseDir, runId);
|
|
36
36
|
}
|
|
37
37
|
await mkdir(this.runDir, { recursive: true });
|
|
38
|
+
await healTornTranscript(this.runDir);
|
|
38
39
|
this.log.info(`Run directory created: ${this.runDir}`);
|
|
39
40
|
return this.runDir;
|
|
40
41
|
}
|
|
@@ -46,6 +47,9 @@ export class RunDiskStore {
|
|
|
46
47
|
})}\n`;
|
|
47
48
|
await appendFile(join(dir, 'transcript.jsonl'), line, 'utf-8');
|
|
48
49
|
}
|
|
50
|
+
async readEvents(options) {
|
|
51
|
+
return readRunEventsIn(this.requireInit(), options);
|
|
52
|
+
}
|
|
49
53
|
/**
|
|
50
54
|
* Every tool call this run has already finished, keyed by `toolUseId`.
|
|
51
55
|
*
|
|
@@ -261,6 +265,103 @@ export class RunDiskStore {
|
|
|
261
265
|
}
|
|
262
266
|
}
|
|
263
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Every durable event under one run directory, oldest first.
|
|
270
|
+
*
|
|
271
|
+
* A free function for the same reason {@link readCheckpointsIn} is one: a
|
|
272
|
+
* caller catching up on a run this process never started would otherwise have
|
|
273
|
+
* to bind a {@link RunDiskStore} to read it, and binding one CREATES the
|
|
274
|
+
* directory. A read that mints an empty run directory then answers "no events"
|
|
275
|
+
* is indistinguishable from a run that genuinely has none.
|
|
276
|
+
*
|
|
277
|
+
* ## Unsequenced lines take their position
|
|
278
|
+
*
|
|
279
|
+
* A transcript written before events were numbered carries no `seq` at all.
|
|
280
|
+
* Numbering those lines by their 1-based position is what keeps their evidence
|
|
281
|
+
* reachable: a legacy run of five lines reads back as 1..5, seeds the emitter
|
|
282
|
+
* at 5, and its next event is 6 — continuous, and stable on every later read.
|
|
283
|
+
* Skipping them instead would erase a run's whole history from a catch-up, and
|
|
284
|
+
* synthesising nothing at all would put the emitter back at 1 on top of a log
|
|
285
|
+
* that already has five entries.
|
|
286
|
+
*
|
|
287
|
+
* A damaged line is skipped rather than refused, which is the one place this
|
|
288
|
+
* differs from the checkpoint reader next door, and deliberately: a checkpoint
|
|
289
|
+
* is read to RESUME from, so a damaged one must stop the resume, while the
|
|
290
|
+
* transcript is read to REPORT from, and dropping every event after a torn line
|
|
291
|
+
* would be a larger loss than the torn line itself. The position count still
|
|
292
|
+
* advances over it, so the numbering of the events after it is unchanged.
|
|
293
|
+
*/
|
|
294
|
+
export async function readRunEventsIn(runDir, options) {
|
|
295
|
+
let raw;
|
|
296
|
+
try {
|
|
297
|
+
raw = await readFile(join(runDir, 'transcript.jsonl'), 'utf-8');
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
if (isFileNotFound(err))
|
|
301
|
+
return [];
|
|
302
|
+
throw err;
|
|
303
|
+
}
|
|
304
|
+
const sinceSeq = options?.sinceSeq ?? 0;
|
|
305
|
+
const events = [];
|
|
306
|
+
let position = 0;
|
|
307
|
+
for (const line of raw.split('\n')) {
|
|
308
|
+
if (line.length === 0)
|
|
309
|
+
continue;
|
|
310
|
+
position += 1;
|
|
311
|
+
let parsed;
|
|
312
|
+
try {
|
|
313
|
+
parsed = JSON.parse(line);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (parsed === null || typeof parsed !== 'object' || typeof parsed.type !== 'string')
|
|
319
|
+
continue;
|
|
320
|
+
const seq = typeof parsed.seq === 'number' ? parsed.seq : position;
|
|
321
|
+
if (seq <= sinceSeq)
|
|
322
|
+
continue;
|
|
323
|
+
events.push({
|
|
324
|
+
...parsed,
|
|
325
|
+
seq,
|
|
326
|
+
// Stamped by `appendEvent` since long before it was declared. A line
|
|
327
|
+
// that predates even that gets the only honest answer available:
|
|
328
|
+
// zero, which sorts before every real moment and cannot be mistaken
|
|
329
|
+
// for one.
|
|
330
|
+
timestamp: typeof parsed.timestamp === 'number' ? parsed.timestamp : 0,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
return events;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Terminate a transcript whose last line was cut off mid-write.
|
|
337
|
+
*
|
|
338
|
+
* A process killed during `appendFile` leaves a fragment with no newline. The
|
|
339
|
+
* next append lands on the same line, so the fragment and a WHOLE, correct
|
|
340
|
+
* event merge into one unparsable line — and the reader skips it. The event was
|
|
341
|
+
* written, the emitter counted it as durable, and it is gone.
|
|
342
|
+
*
|
|
343
|
+
* Ending the fragment is enough. It stays unreadable and is skipped as it
|
|
344
|
+
* always was; everything appended after it survives, which is the difference
|
|
345
|
+
* between losing one event and losing one event plus the next.
|
|
346
|
+
*
|
|
347
|
+
* Called from `initRun`, which is the only moment the store knows nothing is
|
|
348
|
+
* mid-write.
|
|
349
|
+
*/
|
|
350
|
+
async function healTornTranscript(runDir) {
|
|
351
|
+
const path = join(runDir, 'transcript.jsonl');
|
|
352
|
+
let raw;
|
|
353
|
+
try {
|
|
354
|
+
raw = await readFile(path, 'utf-8');
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
if (isFileNotFound(err))
|
|
358
|
+
return;
|
|
359
|
+
throw err;
|
|
360
|
+
}
|
|
361
|
+
if (raw.length === 0 || raw.endsWith('\n'))
|
|
362
|
+
return;
|
|
363
|
+
await appendFile(path, '\n', 'utf-8');
|
|
364
|
+
}
|
|
264
365
|
/**
|
|
265
366
|
* Every checkpoint stored under one run directory, ascending by `createdAt`.
|
|
266
367
|
*
|