mustflow 2.112.8 → 2.112.9

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.
@@ -9,6 +9,7 @@ const LOCK_ROOT_RELATIVE_PATH = '.mustflow/state/locks';
9
9
  export const ACTIVE_RUN_LOCK_ID_ENV = 'MUSTFLOW_ACTIVE_RUN_LOCK_ID';
10
10
  const LOCK_MUTEX_STALE_MS = 30_000;
11
11
  const LOCK_MUTEX_WAIT_MS = 1_000;
12
+ const RUN_STATE_UPDATE_MUTEX_WAIT_MS = 35_000;
12
13
  const LOCK_MUTEX_SLEEP_MS = 25;
13
14
  const LOCK_MUTEX_SLEEP_BUFFER = new Int32Array(new SharedArrayBuffer(4));
14
15
  const LOCK_MUTEX_RECOVERY_DIRECTORY = 'mutex.recovery';
@@ -342,13 +343,14 @@ function recoverStaleMutexWithoutOwner(mutex) {
342
343
  releaseRecovery();
343
344
  }
344
345
  }
345
- function acquireMutex(projectRoot) {
346
+ function acquireMutex(projectRoot, options = {}) {
346
347
  const root = activeLockRoot(projectRoot);
347
348
  const mutex = activeLockMutexDirectory(projectRoot);
348
349
  const ownerPath = path.join(mutex, 'owner.json');
349
350
  const ownerToken = sha256(`${process.pid}:${Date.now()}:${process.hrtime.bigint()}`);
350
351
  mkdirSync(root, { recursive: true });
351
352
  const startedAt = Date.now();
353
+ const waitMs = options.waitMs ?? LOCK_MUTEX_WAIT_MS;
352
354
  while (true) {
353
355
  try {
354
356
  mkdirSync(mutex);
@@ -378,7 +380,7 @@ function acquireMutex(projectRoot) {
378
380
  if (!error || typeof error !== 'object' || !('code' in error) || error.code !== 'EEXIST') {
379
381
  throw error;
380
382
  }
381
- if (Date.now() - startedAt > LOCK_MUTEX_WAIT_MS) {
383
+ if (Date.now() - startedAt > waitMs) {
382
384
  const owner = readMutexOwner(ownerPath);
383
385
  if (owner) {
384
386
  if (mutexOwnerIsStale(owner) && recoverStaleMutexWithOwner(mutex, ownerPath, owner)) {
@@ -414,6 +416,15 @@ function acquireMutex(projectRoot) {
414
416
  }
415
417
  }
416
418
  }
419
+ export function withRunStateUpdateMutex(projectRoot, callback) {
420
+ const releaseMutex = acquireMutex(projectRoot, { waitMs: RUN_STATE_UPDATE_MUTEX_WAIT_MS });
421
+ try {
422
+ return callback();
423
+ }
424
+ finally {
425
+ releaseMutex();
426
+ }
427
+ }
417
428
  export function inspectActiveRunLocks(projectRoot, contract, intentName) {
418
429
  const effects = normalizeCommandEffects(projectRoot, contract, intentName);
419
430
  const records = readActiveRecords(projectRoot);
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { withRunStateUpdateMutex } from './active-run-locks.js';
3
4
  import { writeJsonFileInsideWithoutSymlinks } from './safe-filesystem.js';
4
5
  const PERFORMANCE_HISTORY_SCHEMA_VERSION = '1';
5
6
  const PERFORMANCE_HISTORY_DIR = path.join('.mustflow', 'state', 'perf');
@@ -318,14 +319,16 @@ export function recordRunPerformanceHistory(projectRoot, receipt) {
318
319
  return;
319
320
  }
320
321
  try {
321
- const historyDir = path.join(projectRoot, PERFORMANCE_HISTORY_DIR);
322
- const samplesPath = path.join(historyDir, PERFORMANCE_SAMPLES_FILE);
323
- const summaryPath = path.join(historyDir, PERFORMANCE_SUMMARY_FILE);
324
- const samples = enforceSizeLimit(pruneSamples([...readSamples(samplesPath), sample], sample.observed_day), sample.observed_day);
325
- const samplesFile = createSamplesFile(samples);
326
- const summaryFile = createSummary(samples, sample.observed_day);
327
- writeJsonFileInsideWithoutSymlinks(projectRoot, samplesPath, samplesFile);
328
- writeJsonFileInsideWithoutSymlinks(projectRoot, summaryPath, summaryFile);
322
+ withRunStateUpdateMutex(projectRoot, () => {
323
+ const historyDir = path.join(projectRoot, PERFORMANCE_HISTORY_DIR);
324
+ const samplesPath = path.join(historyDir, PERFORMANCE_SAMPLES_FILE);
325
+ const summaryPath = path.join(historyDir, PERFORMANCE_SUMMARY_FILE);
326
+ const samples = enforceSizeLimit(pruneSamples([...readSamples(samplesPath), sample], sample.observed_day), sample.observed_day);
327
+ const samplesFile = createSamplesFile(samples);
328
+ const summaryFile = createSummary(samples, sample.observed_day);
329
+ writeJsonFileInsideWithoutSymlinks(projectRoot, samplesPath, samplesFile);
330
+ writeJsonFileInsideWithoutSymlinks(projectRoot, summaryPath, summaryFile);
331
+ });
329
332
  }
330
333
  catch {
331
334
  // Performance history is a local optimization hint. A write failure must not affect command execution.
@@ -1,5 +1,6 @@
1
1
  import { performance } from 'node:perf_hooks';
2
2
  import path from 'node:path';
3
+ import { withRunStateUpdateMutex } from './active-run-locks.js';
3
4
  import { writeJsonFileInsideWithoutSymlinks } from './safe-filesystem.js';
4
5
  const RUN_PROFILE_SCHEMA_VERSION = '1';
5
6
  const RUN_PROFILE_ENV = 'MUSTFLOW_RUN_PROFILE';
@@ -75,7 +76,9 @@ export class RunProfiler {
75
76
  profile_path: getProfileRelativePath(),
76
77
  };
77
78
  const profilePath = path.join(input.projectRoot, RUN_PROFILE_DIR, LATEST_RUN_PROFILE);
78
- writeJsonFileInsideWithoutSymlinks(input.projectRoot, profilePath, profile);
79
+ withRunStateUpdateMutex(input.projectRoot, () => {
80
+ writeJsonFileInsideWithoutSymlinks(input.projectRoot, profilePath, profile);
81
+ });
79
82
  }
80
83
  recordPhase(name, startedAtMs) {
81
84
  this.phases.push({
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import path from 'node:path';
3
+ import { withRunStateUpdateMutex } from './active-run-locks.js';
3
4
  import { createStateRunId } from './atomic-state-write.js';
4
5
  import { COMMAND_OUTPUT_LIMIT_SCOPE } from './command-output-limits.js';
5
6
  import { decodeUtf8Tail } from './bounded-output.js';
@@ -295,9 +296,11 @@ export function writeRunReceipt(projectRoot, receipt, policy) {
295
296
  if (relativeToRunDir.startsWith('..') || path.isAbsolute(relativeToRunDir)) {
296
297
  throw new Error(`Run receipt path must stay inside ${RUN_RECEIPT_DIR}`);
297
298
  }
298
- writeJsonFileInsideWithoutSymlinks(projectRoot, receiptPath, receipt);
299
- writeJsonFileInsideWithoutSymlinks(projectRoot, latestPath, receipt);
300
- if (policy) {
301
- updateRunReceiptState(projectRoot, policy);
302
- }
299
+ withRunStateUpdateMutex(projectRoot, () => {
300
+ writeJsonFileInsideWithoutSymlinks(projectRoot, receiptPath, receipt);
301
+ writeJsonFileInsideWithoutSymlinks(projectRoot, latestPath, receipt);
302
+ if (policy) {
303
+ updateRunReceiptState(projectRoot, policy);
304
+ }
305
+ });
303
306
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.112.8",
3
+ "version": "2.112.9",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
@@ -1,6 +1,6 @@
1
1
  id = "default"
2
2
  name = "default"
3
- version = "2.112.8"
3
+ version = "2.112.9"
4
4
  description = "Minimal workflow for LLM agents to read, edit, and verify their work in a repository."
5
5
  common_root = "common"
6
6
  locales_root = "locales"