@vzn/vx 0.0.25 → 0.0.26
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"description": "An open, extensible monorepo task runner.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.26",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.26",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.26",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.26"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
// layer owns the run index (history, stats, prune) so a run is recorded
|
|
4
4
|
// once. Restore goes to the layer that produced the hit — remembered per
|
|
5
5
|
// hash — because an entry's artifact lives wherever it was found.
|
|
6
|
+
//
|
|
7
|
+
// A layer that throws in a lookup is a miss there and the walk goes on; one
|
|
8
|
+
// that throws in a save is skipped and the rest still save. Unisolated, a
|
|
9
|
+
// plugin layer's throw ended the walk before the local floor under it: its
|
|
10
|
+
// `get` failed the task, its `save` kept the entry from every later layer
|
|
11
|
+
// (item 1020). A save that fails in EVERY layer still throws, so the
|
|
12
|
+
// caller's own "cache save failed" line stays the one word on it.
|
|
6
13
|
|
|
7
14
|
import type {
|
|
8
15
|
Cache,
|
|
@@ -21,15 +28,31 @@ import type {
|
|
|
21
28
|
RunRecord,
|
|
22
29
|
} from './cache.js'
|
|
23
30
|
|
|
31
|
+
/** Told of a layer's failure the chain went past: its index, the method, the error. */
|
|
32
|
+
export type LayerErrorReport = (layer: number, method: string, err: unknown) => void
|
|
33
|
+
|
|
24
34
|
export class ChainedCache implements CacheLayer {
|
|
25
35
|
readonly hasRemote: boolean
|
|
26
36
|
private readonly hitLayer = new Map<string, CacheLayer>()
|
|
27
37
|
|
|
28
|
-
constructor(
|
|
38
|
+
constructor(
|
|
39
|
+
readonly layers: readonly CacheLayer[],
|
|
40
|
+
private readonly onLayerError: LayerErrorReport = () => undefined,
|
|
41
|
+
) {
|
|
29
42
|
if (layers.length < 2) throw new Error('ChainedCache needs at least two layers')
|
|
30
43
|
this.hasRemote = layers.some((l) => l.hasRemote === true)
|
|
31
44
|
}
|
|
32
45
|
|
|
46
|
+
/** `layer`'s answer, or `fallback` once its failure is reported. */
|
|
47
|
+
private async ask<T>(layer: CacheLayer, method: string, call: () => Promise<T>, fallback: T) {
|
|
48
|
+
try {
|
|
49
|
+
return await call()
|
|
50
|
+
} catch (err) {
|
|
51
|
+
this.onLayerError(this.layers.indexOf(layer), method, err)
|
|
52
|
+
return fallback
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
get local(): Cache | undefined {
|
|
34
57
|
return this.layers[0]!.local
|
|
35
58
|
}
|
|
@@ -49,11 +72,11 @@ export class ChainedCache implements CacheLayer {
|
|
|
49
72
|
// saved locally, in the outcome, the summary and telemetry (item 889).
|
|
50
73
|
const owner = this.hitLayer.get(hash)
|
|
51
74
|
if (owner !== undefined) {
|
|
52
|
-
const entry = await owner.get(hash, ctx)
|
|
75
|
+
const entry = await this.ask(owner, 'get', () => owner.get(hash, ctx), null)
|
|
53
76
|
if (entry !== null) return entry
|
|
54
77
|
}
|
|
55
78
|
for (const layer of this.layers) {
|
|
56
|
-
const entry = await layer.get(hash, ctx)
|
|
79
|
+
const entry = await this.ask(layer, 'get', () => layer.get(hash, ctx), null)
|
|
57
80
|
if (entry !== null) {
|
|
58
81
|
this.hitLayer.set(hash, layer)
|
|
59
82
|
return entry
|
|
@@ -64,7 +87,7 @@ export class ChainedCache implements CacheLayer {
|
|
|
64
87
|
|
|
65
88
|
async has(hash: string): Promise<'local' | 'remote' | null> {
|
|
66
89
|
for (const layer of this.layers) {
|
|
67
|
-
const where = await layer.has(hash)
|
|
90
|
+
const where = await this.ask(layer, 'has', () => layer.has(hash), null)
|
|
68
91
|
if (where !== null) {
|
|
69
92
|
this.hitLayer.set(hash, layer)
|
|
70
93
|
return where
|
|
@@ -75,7 +98,7 @@ export class ChainedCache implements CacheLayer {
|
|
|
75
98
|
|
|
76
99
|
async prefetch(hash: string, ctx?: CacheGetContext): Promise<boolean> {
|
|
77
100
|
for (const layer of this.layers) {
|
|
78
|
-
if (await layer.prefetch(hash, ctx)) {
|
|
101
|
+
if (await this.ask(layer, 'prefetch', () => layer.prefetch(hash, ctx), false)) {
|
|
79
102
|
this.hitLayer.set(hash, layer)
|
|
80
103
|
return true
|
|
81
104
|
}
|
|
@@ -96,7 +119,12 @@ export class ChainedCache implements CacheLayer {
|
|
|
96
119
|
let complete = true
|
|
97
120
|
for (const layer of this.layers) {
|
|
98
121
|
if (layer.hasRemote !== true) continue
|
|
99
|
-
const found =
|
|
122
|
+
const found = await this.ask(
|
|
123
|
+
layer,
|
|
124
|
+
'remoteHasMany',
|
|
125
|
+
async () => (await layer.remoteHasMany?.(hashes)) ?? null,
|
|
126
|
+
null,
|
|
127
|
+
)
|
|
100
128
|
if (found === null) {
|
|
101
129
|
complete = false
|
|
102
130
|
continue
|
|
@@ -159,12 +187,20 @@ export class ChainedCache implements CacheLayer {
|
|
|
159
187
|
// `skipLocalWrite` and go straight to their remote upload (which reads
|
|
160
188
|
// the artifact the first layer just wrote — same handle, same path).
|
|
161
189
|
const seenLocals = new Set<NonNullable<CacheLayer['local']>>()
|
|
162
|
-
|
|
190
|
+
const failures: Array<[layer: number, err: unknown]> = []
|
|
191
|
+
for (const [i, layer] of this.layers.entries()) {
|
|
163
192
|
const local = layer.local
|
|
164
193
|
const skip = local !== undefined && seenLocals.has(local)
|
|
165
|
-
|
|
194
|
+
try {
|
|
195
|
+
await layer.save(skip ? { ...args, skipLocalWrite: true } : args)
|
|
196
|
+
} catch (err) {
|
|
197
|
+
failures.push([i, err])
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
166
200
|
if (local !== undefined) seenLocals.add(local)
|
|
167
201
|
}
|
|
202
|
+
if (failures.length === this.layers.length) throw failures[0]![1]
|
|
203
|
+
for (const [i, err] of failures) this.onLayerError(i, 'save', err)
|
|
168
204
|
}
|
|
169
205
|
|
|
170
206
|
ingest(hash: string, body: Blob | Response, meta: IngestMeta): Promise<void> {
|
|
@@ -326,6 +326,7 @@ export async function resolveCache(
|
|
|
326
326
|
ctx: CacheContext,
|
|
327
327
|
): Promise<CacheLayer> {
|
|
328
328
|
const layers: CacheLayer[] = []
|
|
329
|
+
const owners = new Map<CacheLayer, string>()
|
|
329
330
|
for (const plugin of plugins) {
|
|
330
331
|
if (plugin.cache === undefined) continue
|
|
331
332
|
const layer = await safe(plugin, 'cache', () => plugin.cache!(ctx))
|
|
@@ -337,6 +338,7 @@ export async function resolveCache(
|
|
|
337
338
|
// passed and died at its first hit inside restoreOutputs.
|
|
338
339
|
assertShape(plugin, 'cache', layer, CACHE_LAYER_METHODS, 'a cache layer')
|
|
339
340
|
layers.push(layer)
|
|
341
|
+
if (!owners.has(layer)) owners.set(layer, `plugin '${plugin.name}'`)
|
|
340
342
|
}
|
|
341
343
|
// Core's own store is the TAIL of the chain — the floor under every
|
|
342
344
|
// lookup, not a plugin a workspace has to declare. A layer that WRAPS
|
|
@@ -345,7 +347,21 @@ export async function resolveCache(
|
|
|
345
347
|
if (!layers.includes(ctx.localCache)) layers.push(ctx.localCache)
|
|
346
348
|
const wrapsLocal = layers.some((l) => l !== ctx.localCache && l.local === ctx.localCache)
|
|
347
349
|
const distinct = wrapsLocal ? layers.filter((l) => l !== ctx.localCache) : layers
|
|
348
|
-
|
|
350
|
+
if (distinct.length === 1) return distinct[0]!
|
|
351
|
+
// One line per layer and method: a layer that throws on every lookup
|
|
352
|
+
// would otherwise print once per task.
|
|
353
|
+
const told = new Set<string>()
|
|
354
|
+
return new ChainedCache(distinct, (i, method, err) => {
|
|
355
|
+
const layer = distinct[i]!
|
|
356
|
+
const who =
|
|
357
|
+
layer === ctx.localCache ? 'the local cache' : (owners.get(layer) ?? 'a cache layer')
|
|
358
|
+
if (told.has(`${i}\0${method}`)) return
|
|
359
|
+
told.add(`${i}\0${method}`)
|
|
360
|
+
const m = err instanceof Error ? err.message : String(err)
|
|
361
|
+
const then =
|
|
362
|
+
method === 'save' ? 'the other layers still save' : 'a miss there; the next layer answers'
|
|
363
|
+
ctx.warn(`[vx] ${who} failed in cache ${method}: ${m}; ${then}`)
|
|
364
|
+
})
|
|
349
365
|
}
|
|
350
366
|
|
|
351
367
|
/**
|