@@ -551,10 +551,11 @@ export function isBundleOwnedEntry(entry: string): boolean {
551551 * names are moved aside or replaced — foreign files in `--out` survive.
552552 *
553553 * Re-commit safety: bundle-owned entries are renamed to a sibling
554- * aside directory before the new artifacts land. `meta.json` is installed
555- * last so its presence remains the completion signal. On failure, aside
556- * entries are restored so a failed re-commit never deletes `meta.json`
557- * without leaving a usable prior bundle (or the prior bundle restored).
554+ * aside directory before the new artifacts land. The prior `meta.json`
555+ * is moved aside first (§3/§7.3 — no meta ⇒ refuse to consume) so a
556+ * concurrent reader never sees meta pointing at steps already gone.
557+ * The new `meta.json` is installed last. On failure, aside entries are
558+ * restored so a failed re-commit never leaves the directory unusable.
558559 */
559560export async function commitBundle (
560561 tmpDir : string ,
@@ -588,9 +589,13 @@ export async function commitBundle(
588589 const newTopLevelSet = new Set ( topLevel ) ;
589590 newTopLevelSet . add ( 'meta.json' ) ;
590591
592+ // meta.json first — its presence is the completion signal; do not
593+ // move steps (or anything else) aside while a stale meta is still visible.
594+ await asideIfPresent ( 'meta.json' ) ;
595+
591596 const existing = await readdir ( dir ) . catch ( ( ) => [ ] as string [ ] ) ;
592597 for ( const entry of existing ) {
593- if ( entry === '.tmp' ) continue ;
598+ if ( entry === '.tmp' || entry === 'meta.json' ) continue ;
594599 if ( ! isBundleOwnedEntry ( entry ) ) continue ;
595600
596601 const isStale = entry !== 'steps' && entry !== '.partial' && ! newTopLevelSet . has ( entry ) ;
0 commit comments