oh-my-copilot 5.6.1 → 5.6.2

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.
@@ -19,7 +19,7 @@ import { join, resolve } from 'path';
19
19
  import { existsSync } from 'fs';
20
20
  import { link, lstat, mkdir, open, readdir, readFile, rm, unlink, writeFile } from 'fs/promises';
21
21
  import { performance } from 'perf_hooks';
22
- import { TeamPaths, absPath, teamStateRoot } from './state-paths.js';
22
+ import { TeamPaths, absPath, canonicalTeamStatePath, teamStateRoot, teamWorkspaceHash } from './state-paths.js';
23
23
  import { getOmcRoot, validateSessionId } from '../lib/worktree-paths.js';
24
24
  import { allocateTasksToWorkers } from './allocation-policy.js';
25
25
  import { readTeamConfig, readWorkerStatus, readWorkerHeartbeat, writeShutdownRequest, readShutdownAck, writeWorkerInbox, saveTeamConfig, commitInitialTeamConfigUnderLock, readRevisionedTeamConfig, saveTeamConfigAtRevision, migrateTeamConfigRevision, withTeamConfigMutationLock, readTeamManifest, } from './monitor.js';
@@ -3412,6 +3412,14 @@ export async function startTeamV2(config) {
3412
3412
  const missing = missingBinaryReasons.map(({ agentType, reason }) => `${agentType}:${reason}`).join(';');
3413
3413
  throw new Error(`cli_binary_preflight_failed:${missing}`);
3414
3414
  }
3415
+ // Clean up any stale reservations from dead processes BEFORE acquiring the lifecycle lock.
3416
+ // This prevents nested lock acquisition and ensures old dead-owner reservations don't block startup.
3417
+ try {
3418
+ await cleanupStaleReservations(sanitized, leaderCwd);
3419
+ }
3420
+ catch {
3421
+ // Best-effort; proceed even if cleanup fails
3422
+ }
3415
3423
  return withTeamInstanceLifecycleLock(leaderCwd, sanitized, async () => {
3416
3424
  // Reserve the name before creating any state, worktree, pane, or provider
3417
3425
  // effect. The reservation remains external and blocks same-name startup
@@ -4938,6 +4946,100 @@ export async function monitorTeamV2(teamName, cwd, expectedInstanceId) {
4938
4946
  };
4939
4947
  }
4940
4948
  // ---------------------------------------------------------------------------
4949
+ // cleanupStaleReservations — remove reservations owned by dead processes
4950
+ // ---------------------------------------------------------------------------
4951
+ /**
4952
+ * Best-effort cleanup of stale team reservations owned by dead processes.
4953
+ * Acquires the lifecycle lock to ensure race-safe removal.
4954
+ * This allows new teams to be created even if a previous team's reservation
4955
+ * file was left behind due to process death during startup.
4956
+ */
4957
+ export async function cleanupStaleReservations(teamName, cwd) {
4958
+ const sanitized = sanitizeTeamName(teamName);
4959
+ try {
4960
+ await withTeamInstanceLifecycleLock(cwd, sanitized, async () => {
4961
+ const workspaceHash = teamWorkspaceHash(cwd, sanitized);
4962
+ const reservationPath = canonicalTeamStatePath(cwd, TeamPaths.teamInstanceReservation(workspaceHash, sanitized));
4963
+ if (!existsSync(reservationPath))
4964
+ return;
4965
+ const contentStr = await readFile(reservationPath, 'utf-8');
4966
+ const reservation = JSON.parse(contentStr);
4967
+ // Validate basic shape
4968
+ if (!(reservation &&
4969
+ typeof reservation === 'object' &&
4970
+ !Array.isArray(reservation) &&
4971
+ 'owner' in reservation &&
4972
+ typeof reservation.owner === 'object')) {
4973
+ return; // Malformed, leave alone
4974
+ }
4975
+ const owner = reservation.owner;
4976
+ if (!(owner &&
4977
+ typeof owner === 'object' &&
4978
+ !Array.isArray(owner) &&
4979
+ 'pid' in owner &&
4980
+ 'process_started_at' in owner)) {
4981
+ return; // Malformed owner, leave alone
4982
+ }
4983
+ // Check if the process is dead
4984
+ const ownerRecord = owner;
4985
+ if (isProcessIdentityDead(ownerRecord)) {
4986
+ // Owner process is dead; clean up this reservation
4987
+ await unlink(reservationPath);
4988
+ }
4989
+ }, 5_000);
4990
+ }
4991
+ catch {
4992
+ // Best-effort; silently proceed on any error
4993
+ }
4994
+ }
4995
+ /**
4996
+ * Clean up abandoned team state when config exists but has no valid instance_id.
4997
+ * This handles partial startup failures and ensures no state is left behind.
4998
+ */
4999
+ export async function cleanupAbandonedTeamState(teamName, cwd) {
5000
+ const sanitized = sanitizeTeamName(teamName);
5001
+ await withTeamInstanceLifecycleLock(cwd, sanitized, async () => {
5002
+ // Clean up the reservation if it exists and owner is dead or missing
5003
+ const workspaceHash = teamWorkspaceHash(cwd, sanitized);
5004
+ const reservationPath = canonicalTeamStatePath(cwd, TeamPaths.teamInstanceReservation(workspaceHash, sanitized));
5005
+ if (existsSync(reservationPath)) {
5006
+ try {
5007
+ const contentStr = await readFile(reservationPath, 'utf-8');
5008
+ const reservation = JSON.parse(contentStr);
5009
+ const owner = reservation.owner;
5010
+ // Remove if owner is dead or if this process owns it
5011
+ if (!owner || isProcessIdentityDead(owner) || ownerMatchesCurrent(owner)) {
5012
+ await unlink(reservationPath);
5013
+ }
5014
+ }
5015
+ catch {
5016
+ // If reservation is malformed, try to remove it anyway
5017
+ try {
5018
+ await unlink(reservationPath);
5019
+ }
5020
+ catch {
5021
+ // Reservation removal failed; continue with state dir cleanup
5022
+ }
5023
+ }
5024
+ }
5025
+ // Clean up the team state directory
5026
+ const teamRoot = teamStateRoot(cwd, sanitized);
5027
+ if (existsSync(teamRoot)) {
5028
+ try {
5029
+ await rm(teamRoot, { recursive: true, force: true });
5030
+ }
5031
+ catch (error) {
5032
+ throw new Error(`Failed to remove team state directory at ${teamRoot}: ${error instanceof Error ? error.message : String(error)}`);
5033
+ }
5034
+ }
5035
+ }, 10_000);
5036
+ }
5037
+ // Helper to check if owner matches current process
5038
+ function ownerMatchesCurrent(owner) {
5039
+ const processStartedAt = currentProcessStartIdentity();
5040
+ return processStartedAt !== null && owner.pid === process.pid && processStartedAt === owner.process_started_at;
5041
+ }
5042
+ // ---------------------------------------------------------------------------
4941
5043
  // shutdownTeam — graceful shutdown with gate, ack, force kill
4942
5044
  // ---------------------------------------------------------------------------
4943
5045
  /**