@namzu/sdk 20.0.0 → 20.2.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 +108 -0
- package/dist/contracts/a2a.d.ts +2 -2
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +11 -0
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +7 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/checkpoint.d.ts +20 -0
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +47 -0
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/store/index.d.ts +3 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +12 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/run/checkpoint-disk.d.ts +56 -2
- package/dist/store/run/checkpoint-disk.d.ts.map +1 -1
- package/dist/store/run/checkpoint-disk.js +83 -2
- package/dist/store/run/checkpoint-disk.js.map +1 -1
- package/dist/store/run/checkpoint-memory.d.ts +31 -0
- package/dist/store/run/checkpoint-memory.d.ts.map +1 -0
- package/dist/store/run/checkpoint-memory.js +83 -0
- package/dist/store/run/checkpoint-memory.js.map +1 -0
- package/dist/store/run/disk.d.ts +53 -0
- package/dist/store/run/disk.d.ts.map +1 -1
- package/dist/store/run/disk.js +73 -30
- package/dist/store/run/disk.js.map +1 -1
- package/dist/store/run/listing.d.ts +75 -0
- package/dist/store/run/listing.d.ts.map +1 -0
- package/dist/store/run/listing.js +242 -0
- package/dist/store/run/listing.js.map +1 -0
- package/dist/types/hitl/index.d.ts +27 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/run/checkpoint-store.d.ts +260 -1
- package/dist/types/run/checkpoint-store.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/manager/run/persistence.ts +18 -4
- package/src/public-runtime.ts +13 -0
- package/src/runtime/query/checkpoint.ts +51 -0
- package/src/store/index.ts +18 -0
- package/src/store/run/checkpoint-disk.ts +128 -4
- package/src/store/run/checkpoint-memory.ts +101 -0
- package/src/store/run/disk.ts +72 -27
- package/src/store/run/listing.ts +307 -0
- package/src/types/hitl/index.ts +28 -0
- package/src/types/run/checkpoint-store.ts +274 -1
package/src/public-runtime.ts
CHANGED
|
@@ -198,12 +198,25 @@ export {
|
|
|
198
198
|
DiskCheckpointStore,
|
|
199
199
|
DiskMemoryStore,
|
|
200
200
|
DiskTaskStore,
|
|
201
|
+
InMemoryCheckpointStore,
|
|
201
202
|
InMemoryMemoryIndex,
|
|
202
203
|
InMemoryMemoryStore,
|
|
203
204
|
InMemoryStore,
|
|
204
205
|
InMemoryTaskStore,
|
|
205
206
|
RunDiskStore,
|
|
206
207
|
} from './store/index.js'
|
|
208
|
+
export type { DiskCheckpointStoreAttribution } from './store/index.js'
|
|
209
|
+
// Enumerating runs above a run id — the read an approval inbox and a park
|
|
210
|
+
// sweep are built from, and the one the contract had no way to express.
|
|
211
|
+
// `listDurableRuns` REFUSES on a store that cannot list rather than
|
|
212
|
+
// reporting an empty page, because "nothing is waiting on a human" is not
|
|
213
|
+
// what "I cannot tell" means.
|
|
214
|
+
export {
|
|
215
|
+
assertContiguousListingScope,
|
|
216
|
+
listDurableRuns,
|
|
217
|
+
paginateDurableRuns,
|
|
218
|
+
toDurableRunEntry,
|
|
219
|
+
} from './store/index.js'
|
|
207
220
|
|
|
208
221
|
export {
|
|
209
222
|
AgentRegistry,
|
|
@@ -51,6 +51,12 @@ export function projectEmergencyToCheckpoint(dump: EmergencySaveData): Iteration
|
|
|
51
51
|
return {
|
|
52
52
|
id: `cp_emergency_${emergencySuffix}` as CheckpointId,
|
|
53
53
|
runId: dump.runId,
|
|
54
|
+
// The dump records the run's start, so this projection carries the
|
|
55
|
+
// same stamp an ordinary checkpoint of that run would — a run whose
|
|
56
|
+
// only surviving record is an emergency dump still has a real
|
|
57
|
+
// attribution instant, and dropping it here would put that run in the
|
|
58
|
+
// "never recorded" bucket for no reason.
|
|
59
|
+
runCreatedAt: dump.startedAt,
|
|
54
60
|
iteration: dump.currentIteration,
|
|
55
61
|
messages: dump.messages,
|
|
56
62
|
tokenUsage: dump.tokenUsage,
|
|
@@ -148,6 +154,27 @@ export class CheckpointManager {
|
|
|
148
154
|
/** See {@link setParkTtl}. */
|
|
149
155
|
private parkTtlMs?: number
|
|
150
156
|
|
|
157
|
+
/**
|
|
158
|
+
* The run's attribution instant, stamped onto every checkpoint this
|
|
159
|
+
* manager writes.
|
|
160
|
+
*
|
|
161
|
+
* Settled exactly once, by whichever of two things happens first, and
|
|
162
|
+
* never reassigned — every write to it below is `??=`, and there are only
|
|
163
|
+
* two:
|
|
164
|
+
*
|
|
165
|
+
* - `restore` ADOPTS it from the checkpoint a resume came back through.
|
|
166
|
+
* A resumed run is the same run, and its creation is already on the
|
|
167
|
+
* record; a fresh process minting a new one would move the key that
|
|
168
|
+
* exists specifically because it does not move.
|
|
169
|
+
* - `create` MINTS it from the run's own start instant when nothing was
|
|
170
|
+
* adopted, which is the fresh-run case.
|
|
171
|
+
*
|
|
172
|
+
* Restore runs during run setup, before the first iteration and therefore
|
|
173
|
+
* before the first `create`, so the adopt always wins on the resume path
|
|
174
|
+
* without either site needing to know which path it is on.
|
|
175
|
+
*/
|
|
176
|
+
private runCreatedAt?: number
|
|
177
|
+
|
|
151
178
|
/**
|
|
152
179
|
* @param store scope-keyed checkpoint persistence. The default query
|
|
153
180
|
* pipeline passes the run's disk-backed store
|
|
@@ -182,9 +209,17 @@ export class CheckpointManager {
|
|
|
182
209
|
workingState?: WorkingStateSnapshot
|
|
183
210
|
},
|
|
184
211
|
): Promise<IterationCheckpoint> {
|
|
212
|
+
// The run's own start, not `Date.now()`. This is meant to say when the
|
|
213
|
+
// run was attributed; taking the clock at the first checkpoint would
|
|
214
|
+
// say when it first became durable, which is a different and later
|
|
215
|
+
// fact, and naming it after the earlier one would make it wrong in
|
|
216
|
+
// exactly the way that is hard to notice.
|
|
217
|
+
this.runCreatedAt ??= runMgr.getSession().startedAt ?? Date.now()
|
|
218
|
+
|
|
185
219
|
const checkpoint: IterationCheckpoint = {
|
|
186
220
|
id: generateCheckpointId(),
|
|
187
221
|
runId: runMgr.id,
|
|
222
|
+
runCreatedAt: this.runCreatedAt,
|
|
188
223
|
iteration,
|
|
189
224
|
messages: [...runMgr.messages],
|
|
190
225
|
tokenUsage: { ...runMgr.tokenUsage },
|
|
@@ -342,6 +377,22 @@ export class CheckpointManager {
|
|
|
342
377
|
details: { checkpointId, runId: this.scope.runId },
|
|
343
378
|
})
|
|
344
379
|
}
|
|
380
|
+
|
|
381
|
+
// Adopt the run's recorded attribution. A resumed run is the SAME run
|
|
382
|
+
// under the same id, and its creation is already on the record; a
|
|
383
|
+
// fresh process minting a new one would step the stamp forward on
|
|
384
|
+
// every resume, which is the motion it exists to avoid.
|
|
385
|
+
//
|
|
386
|
+
// This needs no "is it really my run" guard, and one was written and
|
|
387
|
+
// removed: `readCheckpoint` is keyed by THIS manager's scope, so the
|
|
388
|
+
// only checkpoints reachable here are the ones belonging to
|
|
389
|
+
// `scope.runId`. A replay fork never arrives here at all — it reads
|
|
390
|
+
// its origin through the SOURCE scope in `prepareReplayState` and
|
|
391
|
+
// starts a fresh run, so it mints its own stamp rather than claiming
|
|
392
|
+
// its origin's age. A guard that no input can trip would have read as
|
|
393
|
+
// protection and been none.
|
|
394
|
+
this.runCreatedAt ??= checkpoint.runCreatedAt
|
|
395
|
+
|
|
345
396
|
return checkpoint
|
|
346
397
|
}
|
|
347
398
|
|
package/src/store/index.ts
CHANGED
|
@@ -3,6 +3,24 @@ export type { Identifiable, Timestamped } from './InMemoryStore.js'
|
|
|
3
3
|
|
|
4
4
|
export { RunDiskStore } from './run/disk.js'
|
|
5
5
|
export { DiskCheckpointStore } from './run/checkpoint-disk.js'
|
|
6
|
+
export type { DiskCheckpointStoreAttribution } from './run/checkpoint-disk.js'
|
|
7
|
+
export { InMemoryCheckpointStore } from './run/checkpoint-memory.js'
|
|
8
|
+
// The refusing entry point to the optional listing capability, plus the two
|
|
9
|
+
// projections a host implementing its own backend actually calls: one turns
|
|
10
|
+
// a run's checkpoints into a row, the other applies the contract's filter,
|
|
11
|
+
// ordering and cursor. Re-deriving either is how two stores start
|
|
12
|
+
// disagreeing about what "outstanding" means or where a page ends.
|
|
13
|
+
//
|
|
14
|
+
// `summarizePark` and `DEFAULT_DURABLE_RUN_LIMIT` are deliberately NOT here.
|
|
15
|
+
// The first is an internal of `toDurableRunEntry` and no caller wants half a
|
|
16
|
+
// row; the second is a number a host reads by omitting `limit`. A name a
|
|
17
|
+
// host has no use for is surface to keep correct forever for nobody.
|
|
18
|
+
export {
|
|
19
|
+
assertContiguousListingScope,
|
|
20
|
+
listDurableRuns,
|
|
21
|
+
paginateDurableRuns,
|
|
22
|
+
toDurableRunEntry,
|
|
23
|
+
} from './run/listing.js'
|
|
6
24
|
|
|
7
25
|
export { ActivityStore } from './activity/memory.js'
|
|
8
26
|
export type { ActivityEvent, ActivityEventListener } from './activity/memory.js'
|
|
@@ -1,8 +1,40 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { NamzuError } from '../../types/errors/index.js'
|
|
1
4
|
import type { CheckpointId, IterationCheckpoint } from '../../types/hitl/index.js'
|
|
2
|
-
import type { RunId } from '../../types/ids/index.js'
|
|
3
|
-
import type {
|
|
5
|
+
import type { RunId, SessionId, TenantId } from '../../types/ids/index.js'
|
|
6
|
+
import type {
|
|
7
|
+
CheckpointListingScope,
|
|
8
|
+
CheckpointRunScope,
|
|
9
|
+
CheckpointStore,
|
|
10
|
+
DurableRunEntry,
|
|
11
|
+
DurableRunPage,
|
|
12
|
+
ListDurableRunsOptions,
|
|
13
|
+
} from '../../types/run/checkpoint-store.js'
|
|
4
14
|
import type { RunStoreConfig } from '../../types/run/index.js'
|
|
5
|
-
import {
|
|
15
|
+
import type { ProjectId } from '../../types/session/ids.js'
|
|
16
|
+
import { RunDiskStore, readCheckpointsIn } from './disk.js'
|
|
17
|
+
import { assertContiguousListingScope, paginateDurableRuns, toDurableRunEntry } from './listing.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The attribution a disk store's own layout does not record.
|
|
21
|
+
*
|
|
22
|
+
* The canonical layout is
|
|
23
|
+
* `{root}/projects/{projectId}/sessions/{sessionId}/runs/{runId}` — there is
|
|
24
|
+
* no tenant segment anywhere in it, and `baseDir` is already one session's
|
|
25
|
+
* `runs/` directory, so the project and session are implicit in a string the
|
|
26
|
+
* store cannot parse back out without knowing the layout that built it.
|
|
27
|
+
*
|
|
28
|
+
* A per-run read never needed any of it: the caller supplies a full
|
|
29
|
+
* `CheckpointRunScope` and the store only uses `runId`. A LISTING does — its
|
|
30
|
+
* rows have to be addressable, and a row with no tenant is a row nothing can
|
|
31
|
+
* be resumed from. So the store is told, once, what tree it is holding.
|
|
32
|
+
*/
|
|
33
|
+
export interface DiskCheckpointStoreAttribution {
|
|
34
|
+
readonly tenantId: TenantId
|
|
35
|
+
readonly projectId: ProjectId
|
|
36
|
+
readonly sessionId: SessionId
|
|
37
|
+
}
|
|
6
38
|
|
|
7
39
|
/**
|
|
8
40
|
* Disk conformance layer for {@link CheckpointStore}: adapts the existing
|
|
@@ -20,10 +52,20 @@ import { RunDiskStore } from './disk.js'
|
|
|
20
52
|
*/
|
|
21
53
|
export class DiskCheckpointStore implements CheckpointStore {
|
|
22
54
|
private readonly config: RunStoreConfig
|
|
55
|
+
private readonly attribution?: DiskCheckpointStoreAttribution
|
|
23
56
|
private readonly bound = new Map<RunId, Promise<RunDiskStore>>()
|
|
24
57
|
|
|
25
|
-
|
|
58
|
+
/**
|
|
59
|
+
* @param config the run-store config; `baseDir` is one session's `runs/`
|
|
60
|
+
* directory.
|
|
61
|
+
* @param attribution what tree this is, for
|
|
62
|
+
* {@link DiskCheckpointStore.listDurableRuns}. Optional so that adding
|
|
63
|
+
* the listing did not change an existing construction; a store built
|
|
64
|
+
* without it refuses to list rather than inventing a tenant.
|
|
65
|
+
*/
|
|
66
|
+
constructor(config: RunStoreConfig, attribution?: DiskCheckpointStoreAttribution) {
|
|
26
67
|
this.config = config
|
|
68
|
+
this.attribution = attribution
|
|
27
69
|
}
|
|
28
70
|
|
|
29
71
|
private bind(scope: CheckpointRunScope): Promise<RunDiskStore> {
|
|
@@ -64,4 +106,86 @@ export class DiskCheckpointStore implements CheckpointStore {
|
|
|
64
106
|
const store = await this.bind(scope)
|
|
65
107
|
await store.deleteCheckpoint(checkpointId)
|
|
66
108
|
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Every run with checkpoints under this store's tree.
|
|
112
|
+
*
|
|
113
|
+
* Reads the directories rather than binding a {@link RunDiskStore} per
|
|
114
|
+
* run, because binding CREATES the run directory — a listing that
|
|
115
|
+
* materialized a directory for every run it looked at would grow the tree
|
|
116
|
+
* it is reporting on.
|
|
117
|
+
*
|
|
118
|
+
* ### Why a two-level walk reaches every depth
|
|
119
|
+
*
|
|
120
|
+
* `initRun` nests exactly one level: a run with a parent goes to
|
|
121
|
+
* `{baseDir}/{parentRunId}/children/{runId}`, and a grandchild goes to
|
|
122
|
+
* `{baseDir}/{itsOwnParentRunId}/children/{runId}` — beside the top-level
|
|
123
|
+
* runs, not beneath its grandparent. So the tree is flat-with-one-nesting
|
|
124
|
+
* at every depth, `{baseDir}/*` plus `{baseDir}/* /children/*` enumerates
|
|
125
|
+
* all of it, and each run's `parentRunId` is the directory it sits under.
|
|
126
|
+
* A deep run leaves a bare shell directory under its own id at the top
|
|
127
|
+
* level (`{baseDir}/{parentRunId}` created by `mkdir -p` for a child of a
|
|
128
|
+
* run whose own data lives elsewhere); those hold no `checkpoints/` and
|
|
129
|
+
* drop out as entries with no durable state.
|
|
130
|
+
*/
|
|
131
|
+
async listDurableRuns(
|
|
132
|
+
scope: CheckpointListingScope,
|
|
133
|
+
options?: ListDurableRunsOptions,
|
|
134
|
+
): Promise<DurableRunPage> {
|
|
135
|
+
assertContiguousListingScope(scope, 'DiskCheckpointStore.listDurableRuns')
|
|
136
|
+
|
|
137
|
+
const attribution = this.attribution
|
|
138
|
+
if (!attribution) {
|
|
139
|
+
throw new NamzuError({
|
|
140
|
+
code: 'invalid_config',
|
|
141
|
+
message:
|
|
142
|
+
'DiskCheckpointStore.listDurableRuns: this store was constructed without attribution, so it cannot say which tenant, project or session its runs belong to — and a listing row that carries no scope is a row nothing can be resumed or swept from. Pass the second constructor argument. Refusing rather than returning rows stamped with a guessed tenant.',
|
|
143
|
+
details: { baseDir: this.config.baseDir },
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// A listing is scoped, not addressed: a query for another tenant is a
|
|
148
|
+
// question this tree has no rows for, not an isolation violation. Same
|
|
149
|
+
// reasoning `SessionStore.listSessions` already states for sessions
|
|
150
|
+
// that happen to share a thread id across tenants.
|
|
151
|
+
if (
|
|
152
|
+
scope.tenantId !== attribution.tenantId ||
|
|
153
|
+
(scope.projectId !== undefined && scope.projectId !== attribution.projectId) ||
|
|
154
|
+
(scope.sessionId !== undefined && scope.sessionId !== attribution.sessionId)
|
|
155
|
+
) {
|
|
156
|
+
return { entries: [] }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const now = options?.now ?? Date.now()
|
|
160
|
+
const entries: DurableRunEntry[] = []
|
|
161
|
+
|
|
162
|
+
for (const runId of await this.readRunDirs(this.config.baseDir)) {
|
|
163
|
+
const runDir = join(this.config.baseDir, runId)
|
|
164
|
+
|
|
165
|
+
const own = toDurableRunEntry({ ...attribution, runId }, await readCheckpointsIn(runDir), now)
|
|
166
|
+
if (own) entries.push(own)
|
|
167
|
+
|
|
168
|
+
for (const childId of await this.readRunDirs(join(runDir, 'children'))) {
|
|
169
|
+
const child = toDurableRunEntry(
|
|
170
|
+
{ ...attribution, runId: childId, parentRunId: runId },
|
|
171
|
+
await readCheckpointsIn(join(runDir, 'children', childId)),
|
|
172
|
+
now,
|
|
173
|
+
)
|
|
174
|
+
if (child) entries.push(child)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return paginateDurableRuns(entries, options)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Directory names under `dir`, or none when `dir` does not exist. */
|
|
182
|
+
private async readRunDirs(dir: string): Promise<RunId[]> {
|
|
183
|
+
try {
|
|
184
|
+
const found = await readdir(dir, { withFileTypes: true })
|
|
185
|
+
return found.filter((e) => e.isDirectory()).map((e) => e.name as RunId)
|
|
186
|
+
} catch (err) {
|
|
187
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return []
|
|
188
|
+
throw err
|
|
189
|
+
}
|
|
190
|
+
}
|
|
67
191
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { CheckpointId, IterationCheckpoint } from '../../types/hitl/index.js'
|
|
2
|
+
import type {
|
|
3
|
+
CheckpointListingScope,
|
|
4
|
+
CheckpointRunScope,
|
|
5
|
+
CheckpointStore,
|
|
6
|
+
DurableRunEntry,
|
|
7
|
+
DurableRunPage,
|
|
8
|
+
ListDurableRunsOptions,
|
|
9
|
+
} from '../../types/run/checkpoint-store.js'
|
|
10
|
+
import { assertContiguousListingScope, paginateDurableRuns, toDurableRunEntry } from './listing.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Process-local {@link CheckpointStore}, keyed by the full five-layer scope.
|
|
14
|
+
*
|
|
15
|
+
* Shipped rather than left as a test fixture for two reasons. It is the
|
|
16
|
+
* reference a host reads when writing a backend of its own — the disk store
|
|
17
|
+
* is path-addressed and answers "what does an attribution-keyed store look
|
|
18
|
+
* like" with a directory layout, which is the wrong lesson. And it is the
|
|
19
|
+
* only implementation that can hold more than one tenant at once, because
|
|
20
|
+
* the disk layout has no tenant in it: a test that two tenants' listings
|
|
21
|
+
* stay separate is not expressible against disk, and a rule that cannot be
|
|
22
|
+
* tested on the store a host will actually inject is a rule on paper.
|
|
23
|
+
*
|
|
24
|
+
* Not durable, deliberately: it is for tests, for a single-process host that
|
|
25
|
+
* genuinely wants checkpoints to die with the process, and as the parity
|
|
26
|
+
* partner that proves the listing contract is not a filesystem in disguise.
|
|
27
|
+
*/
|
|
28
|
+
export class InMemoryCheckpointStore implements CheckpointStore {
|
|
29
|
+
/** `tenant/project/session/run` → checkpoint id → checkpoint. */
|
|
30
|
+
private readonly runs = new Map<string, Map<CheckpointId, IterationCheckpoint>>()
|
|
31
|
+
/** Same key → the run's scope, so a listing can rebuild an addressable entry. */
|
|
32
|
+
private readonly scopes = new Map<string, CheckpointRunScope>()
|
|
33
|
+
|
|
34
|
+
private key(scope: CheckpointRunScope): string {
|
|
35
|
+
return [scope.tenantId, scope.projectId, scope.sessionId, scope.runId].join('/')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async writeCheckpoint(scope: CheckpointRunScope, checkpoint: IterationCheckpoint): Promise<void> {
|
|
39
|
+
const key = this.key(scope)
|
|
40
|
+
let run = this.runs.get(key)
|
|
41
|
+
if (!run) {
|
|
42
|
+
run = new Map()
|
|
43
|
+
this.runs.set(key, run)
|
|
44
|
+
}
|
|
45
|
+
// The run's scope is kept beside its checkpoints because the key is a
|
|
46
|
+
// joined string and a listing has to hand back the parts — above all
|
|
47
|
+
// `parentRunId`, which is what makes a sub-run's row addressable.
|
|
48
|
+
//
|
|
49
|
+
// Written on every call rather than only the first, and that is
|
|
50
|
+
// simplicity, not defence: a run's scope is fixed when the run is
|
|
51
|
+
// constructed, so the two cannot differ, and a `has` guard here would
|
|
52
|
+
// be a branch no input can take.
|
|
53
|
+
this.scopes.set(key, {
|
|
54
|
+
tenantId: scope.tenantId,
|
|
55
|
+
projectId: scope.projectId,
|
|
56
|
+
sessionId: scope.sessionId,
|
|
57
|
+
runId: scope.runId,
|
|
58
|
+
...(scope.parentRunId ? { parentRunId: scope.parentRunId } : {}),
|
|
59
|
+
})
|
|
60
|
+
run.set(checkpoint.id, checkpoint)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async readCheckpoint(
|
|
64
|
+
scope: CheckpointRunScope,
|
|
65
|
+
checkpointId: CheckpointId,
|
|
66
|
+
): Promise<IterationCheckpoint | null> {
|
|
67
|
+
return this.runs.get(this.key(scope))?.get(checkpointId) ?? null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async listCheckpoints(scope: CheckpointRunScope): Promise<IterationCheckpoint[]> {
|
|
71
|
+
const run = this.runs.get(this.key(scope))
|
|
72
|
+
if (!run) return []
|
|
73
|
+
return [...run.values()].sort((a, b) => a.createdAt - b.createdAt)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async deleteCheckpoint(scope: CheckpointRunScope, checkpointId: CheckpointId): Promise<void> {
|
|
77
|
+
this.runs.get(this.key(scope))?.delete(checkpointId)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async listDurableRuns(
|
|
81
|
+
scope: CheckpointListingScope,
|
|
82
|
+
options?: ListDurableRunsOptions,
|
|
83
|
+
): Promise<DurableRunPage> {
|
|
84
|
+
assertContiguousListingScope(scope, 'InMemoryCheckpointStore.listDurableRuns')
|
|
85
|
+
const now = options?.now ?? Date.now()
|
|
86
|
+
|
|
87
|
+
const entries: DurableRunEntry[] = []
|
|
88
|
+
for (const [key, checkpoints] of this.runs) {
|
|
89
|
+
const runScope = this.scopes.get(key)
|
|
90
|
+
if (!runScope) continue
|
|
91
|
+
if (runScope.tenantId !== scope.tenantId) continue
|
|
92
|
+
if (scope.projectId !== undefined && runScope.projectId !== scope.projectId) continue
|
|
93
|
+
if (scope.sessionId !== undefined && runScope.sessionId !== scope.sessionId) continue
|
|
94
|
+
|
|
95
|
+
const entry = toDurableRunEntry(runScope, [...checkpoints.values()], now)
|
|
96
|
+
if (entry) entries.push(entry)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return paginateDurableRuns(entries, options)
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/store/run/disk.ts
CHANGED
|
@@ -192,33 +192,7 @@ export class RunDiskStore {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
async listCheckpoints(): Promise<IterationCheckpoint[]> {
|
|
195
|
-
|
|
196
|
-
const cpDir = join(dir, 'checkpoints')
|
|
197
|
-
try {
|
|
198
|
-
const files = await readdir(cpDir)
|
|
199
|
-
const checkpoints: IterationCheckpoint[] = []
|
|
200
|
-
for (const file of files) {
|
|
201
|
-
if (!file.endsWith('.json')) continue
|
|
202
|
-
// An unreadable checkpoint used to be logged and skipped, so
|
|
203
|
-
// this returned a silently short list that four callers treat
|
|
204
|
-
// as complete. A missing NEWEST checkpoint quietly resumes
|
|
205
|
-
// from an older point and re-runs a whole iteration of tool
|
|
206
|
-
// calls; a missing PARKED one reports "not parked" and drops
|
|
207
|
-
// an approval a human already granted, because the file is
|
|
208
|
-
// the only durable record of a park. Pruning under-deletes
|
|
209
|
-
// too: a file the keep-count cannot see is immortal.
|
|
210
|
-
//
|
|
211
|
-
// The by-id read next door was already strict. Two read paths
|
|
212
|
-
// disagreeing about whether damage matters is how the lenient
|
|
213
|
-
// one gets trusted.
|
|
214
|
-
const content = await readFile(join(cpDir, file), 'utf-8')
|
|
215
|
-
checkpoints.push(parseCheckpoint(content, file))
|
|
216
|
-
}
|
|
217
|
-
return checkpoints.sort((a, b) => a.createdAt - b.createdAt)
|
|
218
|
-
} catch (err) {
|
|
219
|
-
if (isFileNotFound(err)) return []
|
|
220
|
-
throw err
|
|
221
|
-
}
|
|
195
|
+
return readCheckpointsIn(this.requireInit())
|
|
222
196
|
}
|
|
223
197
|
|
|
224
198
|
async deleteCheckpoint(checkpointId: CheckpointId): Promise<void> {
|
|
@@ -230,6 +204,29 @@ export class RunDiskStore {
|
|
|
230
204
|
}
|
|
231
205
|
}
|
|
232
206
|
|
|
207
|
+
/**
|
|
208
|
+
* @deprecated Superseded by
|
|
209
|
+
* {@link import('../../types/run/checkpoint-store.js').CheckpointStore.listDurableRuns},
|
|
210
|
+
* reached through {@link import('./listing.js').listDurableRuns}.
|
|
211
|
+
* Removed in the next major.
|
|
212
|
+
*
|
|
213
|
+
* Three things are wrong with `index.json` as the answer to "which runs
|
|
214
|
+
* are there":
|
|
215
|
+
*
|
|
216
|
+
* 1. Its entries carry no tenant, project or session, so a row cannot be
|
|
217
|
+
* turned back into an addressable scope — nothing can be resumed or
|
|
218
|
+
* swept from it.
|
|
219
|
+
* 2. `addToIndex` skips every sub-run, so an inbox built on it drops
|
|
220
|
+
* every approval raised by delegated work, and the symptom looks like
|
|
221
|
+
* a hung specialist rather than a blind listing.
|
|
222
|
+
* 3. It is a catalogue of runs that STARTED, not of runs with durable
|
|
223
|
+
* state, so it cannot tell a run something could resume from one that
|
|
224
|
+
* left nothing behind.
|
|
225
|
+
*
|
|
226
|
+
* Deprecated for one minor rather than deleted outright: this is public
|
|
227
|
+
* surface and a consumer calling it today gets real data back, so the
|
|
228
|
+
* deprecate-before-you-remove rule applies.
|
|
229
|
+
*/
|
|
233
230
|
static async listRuns(baseDir: string): Promise<
|
|
234
231
|
Array<{
|
|
235
232
|
id: string
|
|
@@ -297,6 +294,54 @@ export class RunDiskStore {
|
|
|
297
294
|
}
|
|
298
295
|
}
|
|
299
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Every checkpoint stored under one run directory, ascending by `createdAt`.
|
|
299
|
+
*
|
|
300
|
+
* A free function rather than a method because the scope-level listing walks
|
|
301
|
+
* run directories it has never bound a {@link RunDiskStore} to — and binding
|
|
302
|
+
* one would CREATE the directory, which is not something a read should do.
|
|
303
|
+
* Sharing the function is what keeps the two read paths from disagreeing
|
|
304
|
+
* about what a damaged file means.
|
|
305
|
+
*
|
|
306
|
+
* An unreadable checkpoint used to be logged and skipped, so this returned a
|
|
307
|
+
* silently short list that four callers treat as complete. A missing NEWEST
|
|
308
|
+
* checkpoint quietly resumes from an older point and re-runs a whole
|
|
309
|
+
* iteration of tool calls; a missing PARKED one reports "not parked" and
|
|
310
|
+
* drops an approval a human already granted, because the file is the only
|
|
311
|
+
* durable record of a park. Pruning under-deletes too: a file the keep-count
|
|
312
|
+
* cannot see is immortal. The by-id read next door was already strict, and
|
|
313
|
+
* two read paths disagreeing about whether damage matters is how the lenient
|
|
314
|
+
* one gets trusted.
|
|
315
|
+
*
|
|
316
|
+
* The same reasoning carries up to the listing, which is why the throw
|
|
317
|
+
* propagates there rather than dropping the run: a damaged checkpoint that
|
|
318
|
+
* removed a run from an approval inbox is the missing-park failure again,
|
|
319
|
+
* one level up.
|
|
320
|
+
*
|
|
321
|
+
* A missing `checkpoints/` directory is the only absence that reads as
|
|
322
|
+
* empty — the run genuinely has none. A file that disappears BETWEEN the
|
|
323
|
+
* directory listing and its read throws, where the old shape returned the
|
|
324
|
+
* empty array and discarded every checkpoint it had already parsed.
|
|
325
|
+
*/
|
|
326
|
+
export async function readCheckpointsIn(runDir: string): Promise<IterationCheckpoint[]> {
|
|
327
|
+
const cpDir = join(runDir, 'checkpoints')
|
|
328
|
+
let files: string[]
|
|
329
|
+
try {
|
|
330
|
+
files = await readdir(cpDir)
|
|
331
|
+
} catch (err) {
|
|
332
|
+
if (isFileNotFound(err)) return []
|
|
333
|
+
throw err
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const checkpoints: IterationCheckpoint[] = []
|
|
337
|
+
for (const file of files) {
|
|
338
|
+
if (!file.endsWith('.json')) continue
|
|
339
|
+
const content = await readFile(join(cpDir, file), 'utf-8')
|
|
340
|
+
checkpoints.push(parseCheckpoint(content, file))
|
|
341
|
+
}
|
|
342
|
+
return checkpoints.sort((a, b) => a.createdAt - b.createdAt)
|
|
343
|
+
}
|
|
344
|
+
|
|
300
345
|
async function atomicWriteJson(filePath: string, value: unknown): Promise<void> {
|
|
301
346
|
await atomicWriteFile(filePath, JSON.stringify(stamp(SCHEMA, value), null, 2))
|
|
302
347
|
}
|