@vzn/vx 0.0.637 → 0.0.638
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/docs/caching.md +41 -14
- package/docs/cli.md +115 -73
- package/docs/execution.md +1 -1
- package/docs/features.md +6 -5
- package/docs/schema.md +24 -15
- package/package.json +5 -5
- package/schemas/plan.json +2 -2
- package/skills/vx/SKILL.md +13 -2
- package/src/bin.ts +13 -7
- package/src/cache/cache.ts +21 -2
- package/src/cache/chained-cache.ts +4 -0
- package/src/cache/index.ts +9 -1
- package/src/cache/inputs.ts +0 -24
- package/src/cache/layer.ts +7 -1
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/plugin-cache.ts +248 -0
- package/src/cache/policy.ts +9 -0
- package/src/cache/run-history.ts +68 -0
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +38 -9
- package/src/cache/scoped-cache.ts +15 -2
- package/src/cli/cache.ts +33 -16
- package/src/cli/completions.ts +9 -5
- package/src/cli/help.ts +16 -7
- package/src/cli/index.ts +25 -11
- package/src/cli/info.ts +6 -5
- package/src/cli/init.ts +14 -15
- package/src/cli/last.ts +14 -7
- package/src/cli/lock.ts +39 -27
- package/src/cli/plan-format.ts +5 -3
- package/src/cli/plugin-commands.ts +77 -6
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +14 -12
- package/src/cli/select.ts +18 -9
- package/src/cli/show.ts +66 -40
- package/src/cli/task-verb.ts +4 -4
- package/src/cli/upgrade.ts +20 -11
- package/src/cli/watch-filter.ts +7 -2
- package/src/cli/watch-judge.ts +6 -1
- package/src/cli/watch.ts +44 -20
- package/src/cli/why.ts +10 -1
- package/src/cli/workspace-config.ts +2 -2
- package/src/graph/scheduler.ts +8 -0
- package/src/graph/task-graph.ts +32 -4
- package/src/index.ts +11 -1
- package/src/orchestrator/affected-tasks.ts +8 -3
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/execute-task.ts +26 -43
- package/src/orchestrator/failure-mode.ts +19 -10
- package/src/orchestrator/framed-output.ts +1 -2
- package/src/orchestrator/history.ts +60 -4
- package/src/orchestrator/index.ts +2 -1
- package/src/orchestrator/logger.ts +9 -2
- package/src/orchestrator/metrics.ts +7 -3
- package/src/orchestrator/plugin-host.ts +14 -4
- package/src/orchestrator/plugin.ts +80 -8
- package/src/orchestrator/prepare.ts +51 -7
- package/src/orchestrator/projects.ts +3 -0
- package/src/orchestrator/remote-prefetch.ts +9 -5
- package/src/orchestrator/run-records.ts +1 -1
- package/src/orchestrator/run.ts +126 -27
- package/src/orchestrator/status-line.ts +1 -1
- package/src/orchestrator/summary.ts +69 -44
- package/src/orchestrator/telemetry.ts +1 -1
- package/src/settings.ts +12 -1
- package/src/{orchestrator → util}/colors.ts +5 -0
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +32 -0
- package/src/util/index.ts +8 -1
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +31 -0
- package/src/util/ui.ts +186 -0
- package/src/workspace/affected.ts +20 -23
- package/src/workspace/dependency-rules.ts +12 -4
- package/src/workspace/migration.ts +87 -91
- package/src/workspace/workspace.ts +5 -4
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
type CacheLayer,
|
|
15
15
|
type CachePolicy,
|
|
16
16
|
LayeredCache,
|
|
17
|
+
absentPluginCache,
|
|
18
|
+
type PluginCache,
|
|
19
|
+
type PluginCacheRemote,
|
|
17
20
|
} from '../cache/index.js'
|
|
18
21
|
import { localExecutor, type TaskExecutor } from '../exec/index.js'
|
|
19
22
|
import { maskedLine, settleWithin, teardownTimeoutMs, UserError } from '../util/index.js'
|
|
@@ -29,6 +32,7 @@ import type {
|
|
|
29
32
|
KeyHookContext,
|
|
30
33
|
DiscoveredProject,
|
|
31
34
|
ProjectHookContext,
|
|
35
|
+
RunReport,
|
|
32
36
|
ScheduleHookContext,
|
|
33
37
|
VxPlugin,
|
|
34
38
|
WorkspaceHookContext,
|
|
@@ -385,12 +389,14 @@ export async function claimedAffected(
|
|
|
385
389
|
export async function applyScheduleHooks(
|
|
386
390
|
plugins: readonly VxPlugin[],
|
|
387
391
|
nodes: ReadonlyMap<string, TaskNode>,
|
|
388
|
-
ctx: ScheduleHookContext,
|
|
392
|
+
ctx: Omit<ScheduleHookContext, 'cache'>,
|
|
393
|
+
cacheFor: (plugin: string) => PluginCache = () => absentPluginCache,
|
|
389
394
|
): Promise<ReadonlyMap<string, number>> {
|
|
390
395
|
const merged = new Map<string, number>()
|
|
391
396
|
for (const plugin of plugins) {
|
|
392
397
|
if (plugin.schedule === undefined) continue
|
|
393
|
-
const
|
|
398
|
+
const own = { ...ctx, cache: cacheFor(plugin.name) }
|
|
399
|
+
const weights = await safe(plugin, 'schedule', () => plugin.schedule!(nodes, own))
|
|
394
400
|
if (weights === undefined) continue
|
|
395
401
|
// Iterating a string destructures its characters into `[id, w]` pairs
|
|
396
402
|
// that match no task — a plugin returning `'fast'` used to be a no-op
|
|
@@ -478,6 +484,8 @@ export function buildAdmission(
|
|
|
478
484
|
export interface CacheHost extends CacheContext {
|
|
479
485
|
readonly localCache: Cache
|
|
480
486
|
readonly policy: CachePolicy
|
|
487
|
+
/** Filled with each remote and what its policy lets it do, for the plugins' `ctx.cache`. */
|
|
488
|
+
readonly remotes?: PluginCacheRemote[]
|
|
481
489
|
}
|
|
482
490
|
|
|
483
491
|
/**
|
|
@@ -492,7 +500,7 @@ export async function resolveCache(
|
|
|
492
500
|
plugins: readonly VxPlugin[],
|
|
493
501
|
host: CacheHost,
|
|
494
502
|
): Promise<CacheLayer> {
|
|
495
|
-
const { localCache, policy, ...ctx } = host
|
|
503
|
+
const { localCache, policy, remotes, ...ctx } = host
|
|
496
504
|
const layers: CacheLayer[] = []
|
|
497
505
|
const owners: string[] = []
|
|
498
506
|
for (const plugin of plugins) {
|
|
@@ -515,6 +523,7 @@ export async function resolveCache(
|
|
|
515
523
|
remoteRead: policy.remoteRead && access?.read !== false,
|
|
516
524
|
remoteWrite: policy.remoteWrite && access?.write !== false,
|
|
517
525
|
}
|
|
526
|
+
remotes?.push({ remote, read: own.remoteRead, write: own.remoteWrite })
|
|
518
527
|
layers.push(
|
|
519
528
|
new LayeredCache(localCache, remote, {
|
|
520
529
|
policy: own,
|
|
@@ -628,12 +637,13 @@ export async function resolveExecutors(
|
|
|
628
637
|
export async function teardownPlugins(
|
|
629
638
|
plugins: readonly VxPlugin[],
|
|
630
639
|
warn: (message: string) => void,
|
|
640
|
+
report?: RunReport,
|
|
631
641
|
): Promise<void> {
|
|
632
642
|
for (const plugin of plugins) {
|
|
633
643
|
if (plugin.teardown === undefined) continue
|
|
634
644
|
const ms = teardownTimeoutMs()
|
|
635
645
|
try {
|
|
636
|
-
const settled = await settleWithin(Promise.resolve(plugin.teardown()), ms)
|
|
646
|
+
const settled = await settleWithin(Promise.resolve(plugin.teardown(report)), ms)
|
|
637
647
|
// Same rule as the flush above. A teardown that never settles has left
|
|
638
648
|
// whatever it owns un-released, and the run exits anyway — silence would
|
|
639
649
|
// present that as a clean shutdown.
|
|
@@ -11,13 +11,19 @@
|
|
|
11
11
|
import { readFileSync } from 'node:fs'
|
|
12
12
|
import path from 'node:path'
|
|
13
13
|
import { fileURLToPath } from 'node:url'
|
|
14
|
-
import
|
|
14
|
+
import {
|
|
15
|
+
absentPluginCache,
|
|
16
|
+
type Cache,
|
|
17
|
+
type PluginCache,
|
|
18
|
+
type RemoteCacheLayer,
|
|
19
|
+
} from '../cache/index.js'
|
|
15
20
|
import { PLUGIN_PACKAGE, type ProjectConfig, type WorkspaceConfig } from '../config.js'
|
|
16
21
|
import type { TaskExecutor } from '../exec/index.js'
|
|
17
|
-
import type { TaskNode, TaskOutcome } from '../graph/index.js'
|
|
22
|
+
import type { TaskNode, TaskOutcome, TaskStatus } from '../graph/index.js'
|
|
18
23
|
import { maskedLine, nearest, UserError } from '../util/index.js'
|
|
19
24
|
import type { ProjectMeta } from '../workspace/index.js'
|
|
20
25
|
import type { EventBus, RunStartInfo } from './events.js'
|
|
26
|
+
import type { CiContext } from './run-context.js'
|
|
21
27
|
import type { TelemetryContext, TelemetrySink } from './telemetry.js'
|
|
22
28
|
import type { Logger } from './logger.js'
|
|
23
29
|
|
|
@@ -221,15 +227,51 @@ export interface VxPlugin {
|
|
|
221
227
|
* Optional one-time setup per run (validate the workspace, open a
|
|
222
228
|
* connection, read a token). Throwing aborts the run with a clean
|
|
223
229
|
* UserError naming the plugin. It is not a guard over the pipeline: the
|
|
224
|
-
* config, project, cache, graph
|
|
225
|
-
*
|
|
226
|
-
* precedes the executors, the telemetry sinks,
|
|
227
|
-
* task (tests/plugin-e2e.test.ts).
|
|
230
|
+
* config, project, cache, graph and key stages have run by the time it
|
|
231
|
+
* is called, and a plan (`--dry`, `planRun`) never calls it. It
|
|
232
|
+
* precedes the schedule stage, the executors, the telemetry sinks,
|
|
233
|
+
* admission and the first task (tests/plugin-e2e.test.ts).
|
|
228
234
|
*/
|
|
229
235
|
setup?(ctx: PluginSetupContext): void | Promise<void>
|
|
230
236
|
|
|
231
|
-
/**
|
|
232
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Optional teardown at end-of-run (flush a sink, close a socket). Errors
|
|
239
|
+
* are logged, never thrown. A run that ran to its end passes its
|
|
240
|
+
* `RunReport`; every other exit (a refused setup, a stop) passes none.
|
|
241
|
+
*/
|
|
242
|
+
teardown?(run?: RunReport): void | Promise<void>
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* What a finished run hands each plugin's `teardown`: its facts and every
|
|
247
|
+
* task, with the names core kept the task's bytes under. Core keeps them
|
|
248
|
+
* only when a remote takes the run's writes (CI by default): a failed
|
|
249
|
+
* task's log and the files its outputs left, an uncached task's log. Read
|
|
250
|
+
* them with `ctx.cache.getRun(name)`. Core keeps no manifest of its own; a
|
|
251
|
+
* plugin that wants one writes it from here (`ctx.cache.put`).
|
|
252
|
+
*/
|
|
253
|
+
export interface RunReport {
|
|
254
|
+
/** The `run_id` the run's history rows and telemetry carry. */
|
|
255
|
+
readonly runId: string
|
|
256
|
+
/** `HEAD`'s commit, null outside a git worktree. */
|
|
257
|
+
readonly commit: string | null
|
|
258
|
+
/** What the CI provider says about this run: provider, run URL, pipeline, job, attempt. */
|
|
259
|
+
readonly ciRun: CiContext
|
|
260
|
+
readonly ok: boolean
|
|
261
|
+
/** Every task the run decided, in completion order; group tasks are left out. */
|
|
262
|
+
readonly tasks: readonly RunReportTask[]
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface RunReportTask {
|
|
266
|
+
readonly id: string
|
|
267
|
+
readonly status: TaskStatus
|
|
268
|
+
readonly durationMs: number
|
|
269
|
+
/** The task's cache key, when one was computed. */
|
|
270
|
+
readonly key?: string
|
|
271
|
+
/** Its output (both streams, secrets masked), when core kept it: `ctx.cache.getRun(log)`. */
|
|
272
|
+
readonly log?: string
|
|
273
|
+
/** A tar of the files its declared outputs left, when core kept it: `ctx.cache.getRun(outputs)`. */
|
|
274
|
+
readonly outputs?: string
|
|
233
275
|
}
|
|
234
276
|
|
|
235
277
|
/** Shared, read-only context every capability factory receives. */
|
|
@@ -242,6 +284,12 @@ interface BaseContext {
|
|
|
242
284
|
|
|
243
285
|
/** What `setup` receives: the run's lifecycle, observe-only. */
|
|
244
286
|
export interface PluginSetupContext extends BaseContext {
|
|
287
|
+
/** This plugin's entries in the workspace's cache (`PluginCache`). */
|
|
288
|
+
readonly cache: PluginCache
|
|
289
|
+
/** The run is on CI (`CI` set, or a provider core detects). */
|
|
290
|
+
readonly ci: boolean
|
|
291
|
+
/** The run's branch (`currentBranch`): `run.branch`, else git's, else the CI provider's. */
|
|
292
|
+
readonly branch: string | null
|
|
245
293
|
/**
|
|
246
294
|
* Subscribe a lifecycle handler; it leaves with the run. A throw or a
|
|
247
295
|
* rejection warns once and switches the plugin's handlers off for the run.
|
|
@@ -271,6 +319,12 @@ export interface CommandContext extends BaseContext {
|
|
|
271
319
|
* which `vx` a PATH holds.
|
|
272
320
|
*/
|
|
273
321
|
readonly vx: readonly string[]
|
|
322
|
+
/**
|
|
323
|
+
* This plugin's entries in the workspace's cache, as `setup` reaches them
|
|
324
|
+
* (`PluginCache`), read-only: the local store, then each remote the
|
|
325
|
+
* workspace's cache plugins supply, under the run's scope. Opened on first use.
|
|
326
|
+
*/
|
|
327
|
+
readonly cache: PluginCache
|
|
274
328
|
}
|
|
275
329
|
|
|
276
330
|
/** `config` runs before the cache dir is known — it may be what the hook changes. */
|
|
@@ -327,6 +381,13 @@ export interface ProjectHookContext extends BaseContext {
|
|
|
327
381
|
* process.
|
|
328
382
|
*/
|
|
329
383
|
readonly projects: readonly ProjectMeta[]
|
|
384
|
+
/**
|
|
385
|
+
* True when `vx lock` (or `--check`) visits: the one pass that checks the
|
|
386
|
+
* configs, so the place to report what a plugin's mapping could not say.
|
|
387
|
+
* A run checks nothing (the lock model), so such a report there repeats
|
|
388
|
+
* on every run.
|
|
389
|
+
*/
|
|
390
|
+
readonly lock: boolean
|
|
330
391
|
}
|
|
331
392
|
|
|
332
393
|
export interface GraphHookContext extends BaseContext {
|
|
@@ -386,6 +447,8 @@ export interface ScheduleHookContext extends BaseContext {
|
|
|
386
447
|
readonly localCache: Cache
|
|
387
448
|
/** The run's branch (`currentBranch`), the timings a forecast reads first: pass it to `LocalHistoryProvider`. */
|
|
388
449
|
readonly branch: string | null
|
|
450
|
+
/** This plugin's entries in the workspace's cache, as `setup` has them. */
|
|
451
|
+
readonly cache: PluginCache
|
|
389
452
|
}
|
|
390
453
|
|
|
391
454
|
/** A `cache` hook's context: the workspace and the run's scope, never the local store or the policy, which core owns. */
|
|
@@ -619,6 +682,12 @@ export interface InstallPluginsArgs {
|
|
|
619
682
|
* `run:status` channel.
|
|
620
683
|
*/
|
|
621
684
|
warn?: (message: string) => void
|
|
685
|
+
/** Each plugin's `ctx.cache`, by plugin name. Default: a cache with no layer. */
|
|
686
|
+
cache?: (plugin: string) => PluginCache
|
|
687
|
+
/** Default false. */
|
|
688
|
+
ci?: boolean
|
|
689
|
+
/** Default null. */
|
|
690
|
+
branch?: string | null
|
|
622
691
|
}
|
|
623
692
|
|
|
624
693
|
/**
|
|
@@ -675,6 +744,9 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
|
|
|
675
744
|
workspaceRoot,
|
|
676
745
|
cacheDir,
|
|
677
746
|
warn,
|
|
747
|
+
cache: args.cache?.(plugin.name) ?? absentPluginCache,
|
|
748
|
+
ci: args.ci ?? false,
|
|
749
|
+
branch: args.branch ?? null,
|
|
678
750
|
// A subscription made past the hooks leaves with the run like a
|
|
679
751
|
// hook's does: the bus can outlive the run (`RunOptions.bus`), and
|
|
680
752
|
// one left behind hears every later run on it (item 635).
|
|
@@ -19,6 +19,12 @@ import {
|
|
|
19
19
|
parseCachePolicy,
|
|
20
20
|
GitFilesCache,
|
|
21
21
|
LayeredCache,
|
|
22
|
+
absentPluginCache,
|
|
23
|
+
pluginCache,
|
|
24
|
+
putRunEntry,
|
|
25
|
+
writesRunStore,
|
|
26
|
+
type PluginCache,
|
|
27
|
+
type PluginCacheRemote,
|
|
22
28
|
ScopedCache,
|
|
23
29
|
applyGitEnumeration,
|
|
24
30
|
gitPathspecs,
|
|
@@ -116,6 +122,13 @@ export interface PreparedRun {
|
|
|
116
122
|
* Learned on first ask, once per run.
|
|
117
123
|
*/
|
|
118
124
|
branch: () => string | null
|
|
125
|
+
/** Each plugin's `ctx.cache`: the run's local store and remotes, under its policy and scope. */
|
|
126
|
+
cacheFor: (plugin: string) => PluginCache
|
|
127
|
+
/**
|
|
128
|
+
* Keep `bytes` of this run under `name` (run-store.ts); undefined when no
|
|
129
|
+
* remote takes the run's writes, and so core keeps nothing.
|
|
130
|
+
*/
|
|
131
|
+
keepRunEntry: ((name: string, bytes: Uint8Array) => Promise<boolean>) | undefined
|
|
119
132
|
nodes: Map<string, TaskNode>
|
|
120
133
|
/**
|
|
121
134
|
* The tasks `--exclude-dependencies` took out of the schedule: keyed as a
|
|
@@ -451,6 +464,11 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
451
464
|
// winning prevents double-wrapping when the workspace also declares a
|
|
452
465
|
// cache plugin.
|
|
453
466
|
let cache: CacheLayer
|
|
467
|
+
const remotes: PluginCacheRemote[] = []
|
|
468
|
+
if (options.remoteCache) {
|
|
469
|
+
const { remoteCache } = options
|
|
470
|
+
remotes.push({ remote: remoteCache, read: policy.remoteRead, write: policy.remoteWrite })
|
|
471
|
+
}
|
|
454
472
|
try {
|
|
455
473
|
cache = options.remoteCache
|
|
456
474
|
? new LayeredCache(localCache, options.remoteCache, {
|
|
@@ -464,6 +482,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
464
482
|
scope: cacheScope,
|
|
465
483
|
localCache,
|
|
466
484
|
policy,
|
|
485
|
+
remotes,
|
|
467
486
|
})
|
|
468
487
|
} catch (err) {
|
|
469
488
|
// A cache plugin that throws or returns something off-contract is
|
|
@@ -509,8 +528,10 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
509
528
|
localCache,
|
|
510
529
|
hasRemoteLayer,
|
|
511
530
|
cachePolicy: policy,
|
|
531
|
+
keepRunEntry: undefined,
|
|
512
532
|
priorities: () => Promise.resolve(new Map()),
|
|
513
533
|
branch,
|
|
534
|
+
cacheFor: () => absentPluginCache,
|
|
514
535
|
nodes: new Map(),
|
|
515
536
|
keyOnly: new Map(),
|
|
516
537
|
unresolvedTasks,
|
|
@@ -708,16 +729,35 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
708
729
|
})
|
|
709
730
|
}
|
|
710
731
|
}
|
|
732
|
+
const pluginCaches = new Map<string, PluginCache>()
|
|
733
|
+
const cacheFor = (plugin: string): PluginCache => {
|
|
734
|
+
let own = pluginCaches.get(plugin)
|
|
735
|
+
if (own === undefined) {
|
|
736
|
+
own = pluginCache(plugin, {
|
|
737
|
+
db: localCache.dbHandle(),
|
|
738
|
+
policy,
|
|
739
|
+
remotes,
|
|
740
|
+
scope: cacheScope,
|
|
741
|
+
})
|
|
742
|
+
pluginCaches.set(plugin, own)
|
|
743
|
+
}
|
|
744
|
+
return own
|
|
745
|
+
}
|
|
711
746
|
let scheduled: Promise<ReadonlyMap<string, number>> | undefined
|
|
712
747
|
const priorities = (): Promise<ReadonlyMap<string, number>> =>
|
|
713
748
|
(scheduled ??= hasHook(plugins, 'schedule')
|
|
714
|
-
? applyScheduleHooks(
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
749
|
+
? applyScheduleHooks(
|
|
750
|
+
plugins,
|
|
751
|
+
nodes,
|
|
752
|
+
{
|
|
753
|
+
workspaceRoot,
|
|
754
|
+
cacheDir,
|
|
755
|
+
warn: (m) => log.status(m),
|
|
756
|
+
localCache,
|
|
757
|
+
branch: branch(),
|
|
758
|
+
},
|
|
759
|
+
cacheFor,
|
|
760
|
+
)
|
|
721
761
|
: Promise.resolve(new Map()))
|
|
722
762
|
|
|
723
763
|
return {
|
|
@@ -731,6 +771,10 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
731
771
|
cachePolicy: policy,
|
|
732
772
|
priorities,
|
|
733
773
|
branch,
|
|
774
|
+
cacheFor,
|
|
775
|
+
keepRunEntry: writesRunStore(remotes)
|
|
776
|
+
? (name, bytes) => putRunEntry(remotes, cacheScope, name, bytes)
|
|
777
|
+
: undefined,
|
|
734
778
|
nodes,
|
|
735
779
|
keyOnly,
|
|
736
780
|
unresolvedTasks,
|
|
@@ -163,6 +163,8 @@ interface LoadProjectsBase {
|
|
|
163
163
|
*/
|
|
164
164
|
configs: () => Promise<Lockfile>
|
|
165
165
|
warn: (message: string) => void
|
|
166
|
+
/** `vx lock`'s audit: the `project` stage's `ctx.lock`. */
|
|
167
|
+
lock?: boolean
|
|
166
168
|
/**
|
|
167
169
|
* Entries a load in this same process already produced with the same
|
|
168
170
|
* cache dir and lock (the CLI's selection pass, which stages every
|
|
@@ -323,6 +325,7 @@ export async function loadProjects(args: LoadProjectsArgs): Promise<LoadedProjec
|
|
|
323
325
|
name: meta.name,
|
|
324
326
|
dir: meta.dir,
|
|
325
327
|
projects: args.projectMetas,
|
|
328
|
+
lock: args.lock === true,
|
|
326
329
|
},
|
|
327
330
|
(plugin) =>
|
|
328
331
|
validateProjectConfig(
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
// GET per key.
|
|
13
13
|
//
|
|
14
14
|
// HARD SCOPE: this runs ONLY when a remote layer is configured. It
|
|
15
|
-
//
|
|
16
|
-
//
|
|
15
|
+
// asks the local index only which keys it holds (no local get / no
|
|
16
|
+
// isOutputsCurrent), and probes the remote for the rest: a run the local
|
|
17
|
+
// cache answers makes no remote call (M, 2026-10-15). Local-only
|
|
17
18
|
// runs are byte-identical to before; the caller gates on the layer's
|
|
18
19
|
// own `hasRemote`, so a third-party remote layer gets the overlap too.
|
|
19
20
|
// The two batch-probe hooks are OPTIONAL on the contract: a remote layer
|
|
@@ -75,8 +76,11 @@ async function runPrefetch(args: PrefetchArgs): Promise<void> {
|
|
|
75
76
|
// would 404. When the remote can't batch (`null` — an older remote, reads
|
|
76
77
|
// disabled, or a layer that doesn't implement the hook at all), fall back to
|
|
77
78
|
// prefetching every stable key, exactly as before.
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
const held =
|
|
80
|
+
(await args.cache.localHasMany?.([...new Set(stableKeys.map((k) => k.hash))])) ?? null
|
|
81
|
+
let toPrefetch = held === null ? stableKeys : stableKeys.filter((k) => !held.has(k.hash))
|
|
82
|
+
if (toPrefetch.length === 0) return
|
|
83
|
+
const uniqueHashes = [...new Set(toPrefetch.map((k) => k.hash))]
|
|
80
84
|
const present = (await args.cache.remoteHasMany?.(uniqueHashes)) ?? null
|
|
81
85
|
if (present !== null) {
|
|
82
86
|
args.cache.markRemoteAbsent?.(uniqueHashes.filter((h) => !present.has(h)))
|
|
@@ -85,7 +89,7 @@ async function runPrefetch(args: PrefetchArgs): Promise<void> {
|
|
|
85
89
|
// economy: deleting it alone GETs nothing extra, deleting both GETs
|
|
86
90
|
// every absent key. What it decides is which hashes the pump asks
|
|
87
91
|
// the layer for, and that is what the row pins (item 643, 645).
|
|
88
|
-
toPrefetch =
|
|
92
|
+
toPrefetch = toPrefetch.filter((k) => present.has(k.hash))
|
|
89
93
|
if (toPrefetch.length === 0) return
|
|
90
94
|
}
|
|
91
95
|
|
|
@@ -27,7 +27,7 @@ export interface RunRecordsInput {
|
|
|
27
27
|
ok: boolean
|
|
28
28
|
command: string
|
|
29
29
|
requestedTasks: readonly string[]
|
|
30
|
-
/** Already
|
|
30
|
+
/** Already formatted (`formatCachePolicy`), the form the header row stores. */
|
|
31
31
|
cachePolicy: string
|
|
32
32
|
concurrency: number
|
|
33
33
|
flow: InvocationRecord['flow']
|