@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.25",
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.25",
21
- "@vzn/vx-linux-arm64": "0.0.25",
22
- "@vzn/vx-darwin-x64": "0.0.25",
23
- "@vzn/vx-darwin-arm64": "0.0.25"
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(readonly layers: readonly CacheLayer[]) {
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 = (await layer.remoteHasMany?.(hashes)) ?? null
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
- for (const layer of this.layers) {
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
- await layer.save(skip ? { ...args, skipLocalWrite: true } : args)
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
- return distinct.length === 1 ? distinct[0]! : new ChainedCache(distinct)
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
  /**