pi-crew 0.9.32 → 0.9.34

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.
@@ -330,6 +330,21 @@ export function createRunManifest(params: {
330
330
  }
331
331
 
332
332
  export function saveRunManifest(manifest: TeamRunManifest): void {
333
+ // FIX: Capture the cached tasks array + mtime/size BEFORE we invalidate the
334
+ // cache. The previous implementation re-read tasks.json from disk after the
335
+ // manifest write (a JSON.parse + fs.readFileSync per call), which defeated
336
+ // the whole point of the manifest cache on the hot update path. Reusing the
337
+ // already-cached entry is safe because:
338
+ // 1. The cache entry's tasks array matches the tasks array reflected by
339
+ // its tasksMtimeMs/tasksSize — the three values move together.
340
+ // 2. After invalidate, saveRunTasks (or any other writer) would bump the
341
+ // per-stateRoot generation, so a stale cache hit cannot serve.
342
+ // 3. On cache miss, we fall back to tasks: [] with mtime/size 0 — the same
343
+ // shape the original pre-FIX code produced — so behavior is unchanged.
344
+ const cachedBeforeInvalidate = manifestCache.get(manifest.stateRoot);
345
+ const cachedTasks = cachedBeforeInvalidate?.tasks ?? [];
346
+ const cachedTasksMtimeMs = cachedBeforeInvalidate?.tasksMtimeMs ?? 0;
347
+ const cachedTasksSize = cachedBeforeInvalidate?.tasksSize ?? 0;
333
348
  // FIX: Invalidate cache BEFORE atomic write. The order matters for crash
334
349
  // safety: if we invalidated after the write and crashed before invalidation,
335
350
  // the stale cache entry (up to MANIFEST_CACHE_TTL_MS old) could be served.
@@ -341,40 +356,29 @@ export function saveRunManifest(manifest: TeamRunManifest): void {
341
356
  // FIX: Re-populate cache with actual mtime/size so loadRunManifestById
342
357
  // doesn't miss the cache on next read. Without this, every load until
343
358
  // TTL expires would hit disk because cached 0 !== any real mtime.
344
- // NOTE: tasks is set to [] here because saveRunManifest only writes the
345
- // manifest file, not tasks.json. Callers that need tasks should call
346
- // saveRunTasks or loadRunTasks separately. If loadRunManifestById is called
347
- // immediately after saveRunManifest, it may return empty tasks even if
348
- // tasks.json exists on disk — the mtime/size cache check should invalidate
349
- // on next read, but the returned empty tasks array could confuse callers
350
- // that don't re-read.
359
+ // NOTE: tasks is reused from the pre-invalidate cache snapshot above; if no
360
+ // cache existed, tasks is [] (matching pre-FIX behavior). Callers that need
361
+ // fresh tasks should call saveRunTasks or loadRunTasks separately.
351
362
  const manifestStat = fs.statSync(manifestPath);
352
- // Read tasks.json if it exists to avoid cache inconsistency.
353
- // Without this, loadRunManifestById returns empty tasks even when tasks.json
354
- // has real data, causing incorrect task state display.
355
- let tasks: TeamTaskState[] = [];
356
- let tasksMtimeMs = 0;
357
- let tasksSize = 0;
358
- try {
359
- const tasksPath = path.join(manifest.stateRoot, "tasks.json");
360
- const tasksStat = fs.statSync(tasksPath);
361
- tasks = JSON.parse(fs.readFileSync(tasksPath, "utf-8")) as TeamTaskState[];
362
- tasksMtimeMs = tasksStat.mtimeMs;
363
- tasksSize = tasksStat.size;
364
- } catch {
365
- // tasks.json doesn't exist or is invalid — leave tasks empty
366
- }
367
363
  setManifestCache(manifest.stateRoot, {
368
364
  manifest,
369
- tasks,
365
+ tasks: cachedTasks,
370
366
  manifestMtimeMs: manifestStat.mtimeMs,
371
367
  manifestSize: manifestStat.size,
372
- tasksMtimeMs,
373
- tasksSize,
368
+ tasksMtimeMs: cachedTasksMtimeMs,
369
+ tasksSize: cachedTasksSize,
374
370
  });
375
371
  }
376
372
 
377
373
  export async function saveRunManifestAsync(manifest: TeamRunManifest): Promise<void> {
374
+ // FIX: Capture cached tasks array + mtime/size BEFORE invalidating, same
375
+ // rationale as the sync saveRunManifest above. The async path previously
376
+ // always set tasks: [] with mtime/size 0, so any cache hit was guaranteed
377
+ // to look stale to loadRunManifestById until something else wrote tasks.json.
378
+ const cachedBeforeInvalidate = manifestCache.get(manifest.stateRoot);
379
+ const cachedTasks = cachedBeforeInvalidate?.tasks ?? [];
380
+ const cachedTasksMtimeMs = cachedBeforeInvalidate?.tasksMtimeMs ?? 0;
381
+ const cachedTasksSize = cachedBeforeInvalidate?.tasksSize ?? 0;
378
382
  // FIX: Invalidate cache BEFORE atomic write to prevent stale cache serving
379
383
  // after a crash. See saveRunManifest for full explanation.
380
384
  invalidateRunCache(manifest.stateRoot);
@@ -384,11 +388,11 @@ export async function saveRunManifestAsync(manifest: TeamRunManifest): Promise<v
384
388
  const manifestStat = await fs.promises.stat(manifestPath);
385
389
  setManifestCache(manifest.stateRoot, {
386
390
  manifest,
387
- tasks: [],
391
+ tasks: cachedTasks,
388
392
  manifestMtimeMs: manifestStat.mtimeMs,
389
393
  manifestSize: manifestStat.size,
390
- tasksMtimeMs: 0,
391
- tasksSize: 0,
394
+ tasksMtimeMs: cachedTasksMtimeMs,
395
+ tasksSize: cachedTasksSize,
392
396
  });
393
397
  }
394
398