@agimon-ai/doompi-core 0.0.1-alpha.91 → 0.0.1-alpha.92

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.
@@ -78,6 +78,82 @@ function readToken(lockPath) {
78
78
  return;
79
79
  }
80
80
  }
81
+ /** A dead owner is recoverable only when the entire lock record is recognizable. */
82
+ function readRecoverableLock(lockPath, sourcePath) {
83
+ try {
84
+ const stat = node_fs.default.lstatSync(lockPath);
85
+ if (!stat.isFile() || stat.nlink !== 1) return void 0;
86
+ const value = JSON.parse(node_fs.default.readFileSync(lockPath, "utf8"));
87
+ if (!isHistoryObject(value) || value.version !== LOCK_VERSION || value.format !== LOCK_FORMAT || value.sourcePath !== sourcePath || !Number.isSafeInteger(value.pid) || value.pid <= 0 || typeof value.token !== "string" || !/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/iu.test(value.token)) return void 0;
88
+ return value;
89
+ } catch {
90
+ return;
91
+ }
92
+ }
93
+ function ownerMayBeAlive(pid) {
94
+ try {
95
+ process.kill(pid, 0);
96
+ return true;
97
+ } catch (error) {
98
+ return !(isFileSystemError(error) && error.code === "ESRCH");
99
+ }
100
+ }
101
+ /** A guard left by a dead acquirer can be retired without touching its source lock. */
102
+ function retireDeadGuard(guard) {
103
+ try {
104
+ if (!node_fs.default.lstatSync(guard).isDirectory()) return false;
105
+ const entries = node_fs.default.readdirSync(guard);
106
+ if (entries.length !== 1 || !/^owner-[\da-f-]{36}\.json$/iu.test(entries[0])) return false;
107
+ const marker = node_path.default.join(guard, entries[0]);
108
+ const markerStat = node_fs.default.lstatSync(marker);
109
+ if (!markerStat.isFile() || markerStat.nlink !== 1) return false;
110
+ const owner = JSON.parse(node_fs.default.readFileSync(marker, "utf8"));
111
+ if (!isHistoryObject(owner) || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) return false;
112
+ if (ownerMayBeAlive(owner.pid)) return false;
113
+ node_fs.default.unlinkSync(marker);
114
+ node_fs.default.rmdirSync(guard);
115
+ return true;
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+ /** All new acquirers hold this guard until their ownership record is durable. */
121
+ function withAcquisitionGuard(lockPath, acquire) {
122
+ const guard = `${lockPath}.recovery`;
123
+ try {
124
+ node_fs.default.mkdirSync(guard, { mode: 448 });
125
+ } catch (error) {
126
+ if (!isFileSystemError(error) || error.code !== "EEXIST" || !retireDeadGuard(guard)) throw error;
127
+ node_fs.default.mkdirSync(guard, { mode: 448 });
128
+ }
129
+ const marker = node_path.default.join(guard, `owner-${(0, node_crypto.randomUUID)()}.json`);
130
+ try {
131
+ node_fs.default.writeFileSync(marker, JSON.stringify({ pid: process.pid }), {
132
+ flag: "wx",
133
+ mode: 384
134
+ });
135
+ return acquire();
136
+ } finally {
137
+ node_fs.default.rmSync(marker, { force: true });
138
+ node_fs.default.rmdirSync(guard);
139
+ }
140
+ }
141
+ /** Called only under the acquisition guard, before publishing a new owner. */
142
+ function recoverDeadOwner(lockPath, sourcePath) {
143
+ try {
144
+ const record = readRecoverableLock(lockPath, sourcePath);
145
+ if (!record || ownerMayBeAlive(record.pid)) return false;
146
+ const before = node_fs.default.lstatSync(lockPath);
147
+ if (readRecoverableLock(lockPath, sourcePath)?.token !== record.token || ownerMayBeAlive(record.pid)) return false;
148
+ const current = node_fs.default.lstatSync(lockPath);
149
+ if (before.dev !== current.dev || before.ino !== current.ino) return false;
150
+ node_fs.default.unlinkSync(lockPath);
151
+ return true;
152
+ } catch (error) {
153
+ if (isFileSystemError(error) && error.code === "ENOENT") return false;
154
+ throw error;
155
+ }
156
+ }
81
157
  function releaseLock(lockPath, token, handle) {
82
158
  node_fs.default.closeSync(handle);
83
159
  if (readToken(lockPath) === token) node_fs.default.rmSync(lockPath, { force: true });
@@ -110,8 +186,8 @@ function createLease(locks, sourcePath, assertQuiescent) {
110
186
  /**
111
187
  * Creates the canonical history owner used by runtime writers, export CLI, and
112
188
  * the explicit offline importer. The default source boundary remains v4.
113
- * Locks are deliberately never reclaimed from dead pids: a stale or malformed
114
- * sidecar needs explicit operator cleanup because it cannot prove quiescence.
189
+ * Complete locks left by a dead process are recovered under an exclusive guard.
190
+ * A live owner or an ambiguous sidecar still blocks acquisition.
115
191
  */
116
192
  function createHistoryOwnership(options = {}) {
117
193
  const assertQuiescent = options.assertQuiescent ?? (() => void 0);
@@ -138,15 +214,23 @@ function createHistoryOwnership(options = {}) {
138
214
  recursive: true,
139
215
  mode: 448
140
216
  });
141
- const handle = node_fs.default.openSync(lockPath, "wx", 384);
142
- const lock = {
143
- lockPath,
144
- token,
145
- handle
146
- };
147
- locks.push(lock);
148
- node_fs.default.writeFileSync(handle, `${JSON.stringify(record)}\n`, "utf8");
149
- node_fs.default.fsyncSync(handle);
217
+ withAcquisitionGuard(lockPath, () => {
218
+ let handle;
219
+ try {
220
+ handle = node_fs.default.openSync(lockPath, "wx", 384);
221
+ } catch (error) {
222
+ if (!isFileSystemError(error) || error.code !== "EEXIST" || !recoverDeadOwner(lockPath, lockedPath)) throw error;
223
+ handle = node_fs.default.openSync(lockPath, "wx", 384);
224
+ }
225
+ const lock = {
226
+ lockPath,
227
+ token,
228
+ handle
229
+ };
230
+ locks.push(lock);
231
+ node_fs.default.writeFileSync(handle, `${JSON.stringify(record)}\n`, "utf8");
232
+ node_fs.default.fsyncSync(handle);
233
+ });
150
234
  }
151
235
  assertSource(absoluteSourcePath);
152
236
  } catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"historyOwnership.cjs","names":["path","fs","randomUUID"],"sources":["../src/services/historyOwnership/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { HistoryOwnership, HistoryOwnershipLease } from '../historyImport';\n\nconst LOCK_SUFFIX = '.doompi-v4.lock';\nconst LOCK_VERSION = 1;\nconst LOCK_FORMAT = 'doompi-v4-history-ownership';\n\ninterface HistoryLockRecord {\n version: 1;\n format: 'doompi-v4-history-ownership';\n pid: number;\n sourcePath: string;\n token: string;\n}\n\ninterface HeldHistoryLock {\n lockPath: string;\n token: string;\n handle: number;\n}\n\nexport interface HistoryOwnershipOptions {\n /** Checks managed writers; this cannot account for an unmanaged Pi process. */\n assertQuiescent?: (sourcePath: string) => void | Promise<void>;\n /** The source format admitted by this owner. Defaults to canonical v4. */\n sourceFormat?: 'v3' | 'v4' | 'sqlite';\n /** Additional paths to lock for the lifetime of each acquired source lease. */\n additionalPaths?: readonly string[];\n}\n\nfunction canonicalSourcePath(sourcePath: string): string {\n const absolute = path.resolve(sourcePath);\n if (fs.existsSync(absolute)) return fs.realpathSync(absolute);\n const segments = [path.basename(absolute)];\n let parent = path.dirname(absolute);\n while (!fs.existsSync(parent)) {\n segments.unshift(path.basename(parent));\n parent = path.dirname(parent);\n }\n return path.join(fs.realpathSync(parent), ...segments);\n}\n\n/** Resolve aliases so every writer and exporter contends for the same lock. */\nexport function historyOwnershipLockPath(sourcePath: string): string {\n return `${canonicalSourcePath(sourcePath)}${LOCK_SUFFIX}`;\n}\n\nfunction isHistoryObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isV4Header(value: unknown): boolean {\n return isHistoryObject(value) && value.kind === 'header' && value.v === 4;\n}\n\nfunction isV3Header(value: unknown): boolean {\n return isHistoryObject(value) && value.type === 'session' && value.version === 3;\n}\n\nfunction readHeader(sourcePath: string, format: 'v3' | 'v4'): unknown {\n const firstLine = fs.readFileSync(sourcePath, 'utf8').split('\\n', 1)[0] ?? '';\n try {\n return JSON.parse(firstLine);\n } catch (error) {\n const message =\n format === 'v4'\n ? `History ownership source is not canonical v4 JSONL: ${sourcePath}`\n : `Offline history source is not canonical v3 JSONL: ${sourcePath}`;\n throw new Error(message, { cause: error });\n }\n}\n\nfunction assertIndependentRegularFile(sourcePath: string, label: string): void {\n const stat = fs.statSync(sourcePath);\n if (!stat.isFile() || stat.nlink !== 1) throw new Error(`${label} must be an independent regular file`);\n}\n\nfunction assertV4Source(sourcePath: string): void {\n if (!fs.existsSync(sourcePath)) return;\n assertIndependentRegularFile(sourcePath, 'Canonical history');\n if (!isV4Header(readHeader(sourcePath, 'v4')))\n throw new Error(`History ownership only supports canonical v4 sources: ${sourcePath}`);\n}\n\nfunction assertV3Source(sourcePath: string): void {\n if (!fs.existsSync(sourcePath)) return;\n assertIndependentRegularFile(sourcePath, 'Offline v3 history source');\n if (!isV3Header(readHeader(sourcePath, 'v3')))\n throw new Error(`Offline history import only supports v3 sources: ${sourcePath}`);\n}\n\nfunction assertSqliteSource(sourcePath: string): void {\n if (!fs.existsSync(sourcePath)) return;\n assertIndependentRegularFile(sourcePath, 'SQLite history');\n const descriptor = fs.openSync(sourcePath, 'r');\n try {\n const header = Buffer.alloc(16);\n if (\n fs.readSync(descriptor, header, 0, header.length, 0) !== header.length ||\n header.toString('utf8') !== 'SQLite format 3\\u0000'\n )\n throw new Error(`History ownership source is not SQLite: ${sourcePath}`);\n } finally {\n fs.closeSync(descriptor);\n }\n}\n\nfunction isFileSystemError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && typeof (error as NodeJS.ErrnoException).code === 'string';\n}\n\nfunction readToken(lockPath: string): string | undefined {\n try {\n const value = JSON.parse(fs.readFileSync(lockPath, 'utf8')) as Partial<HistoryLockRecord>;\n return typeof value.token === 'string' ? value.token : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction releaseLock(lockPath: string, token: string, handle: number): void {\n fs.closeSync(handle);\n // An unrecognisable or replaced lock is ambiguous. Never remove it merely\n // because its recorded pid is gone or its contents look stale.\n if (readToken(lockPath) === token) fs.rmSync(lockPath, { force: true });\n}\n\nfunction createLease(\n locks: readonly HeldHistoryLock[],\n sourcePath: string,\n assertQuiescent: (sourcePath: string) => void | Promise<void>,\n): HistoryOwnershipLease {\n let released = false;\n return {\n assertQuiescent: () => {\n if (released) throw new Error('History ownership lease has been released');\n for (const lock of locks) {\n const held = fs.fstatSync(lock.handle);\n const current = fs.statSync(lock.lockPath);\n if (held.dev !== current.dev || held.ino !== current.ino || readToken(lock.lockPath) !== lock.token) {\n throw new Error('History ownership lock was replaced');\n }\n }\n return assertQuiescent(sourcePath);\n },\n release: () => {\n if (released) return;\n released = true;\n let firstError: unknown;\n for (const lock of [...locks].reverse()) {\n try {\n releaseLock(lock.lockPath, lock.token, lock.handle);\n } catch (error) {\n firstError ??= error;\n }\n }\n if (firstError !== undefined) throw firstError;\n },\n };\n}\n\n/**\n * Creates the canonical history owner used by runtime writers, export CLI, and\n * the explicit offline importer. The default source boundary remains v4.\n * Locks are deliberately never reclaimed from dead pids: a stale or malformed\n * sidecar needs explicit operator cleanup because it cannot prove quiescence.\n */\nexport function createHistoryOwnership(options: HistoryOwnershipOptions = {}): HistoryOwnership {\n const assertQuiescent = options.assertQuiescent ?? (() => undefined);\n const assertSource =\n options.sourceFormat === 'sqlite'\n ? assertSqliteSource\n : options.sourceFormat === 'v3'\n ? assertV3Source\n : assertV4Source;\n return {\n async acquire(sourcePath: string): Promise<HistoryOwnershipLease> {\n const absoluteSourcePath = canonicalSourcePath(sourcePath);\n const paths = [absoluteSourcePath, ...(options.additionalPaths ?? [])].map(canonicalSourcePath);\n const canonicalPaths = [...new Set(paths)].sort((left, right) => left.localeCompare(right));\n const locks: HeldHistoryLock[] = [];\n let conflictingLockPath = historyOwnershipLockPath(absoluteSourcePath);\n try {\n for (const lockedPath of canonicalPaths) {\n const lockPath = historyOwnershipLockPath(lockedPath);\n conflictingLockPath = lockPath;\n const token = randomUUID();\n const record: HistoryLockRecord = {\n version: LOCK_VERSION,\n format: LOCK_FORMAT,\n pid: process.pid,\n sourcePath: lockedPath,\n token,\n };\n fs.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });\n const handle = fs.openSync(lockPath, 'wx', 0o600);\n const lock = { lockPath, token, handle };\n locks.push(lock);\n fs.writeFileSync(handle, `${JSON.stringify(record)}\\n`, 'utf8');\n fs.fsyncSync(handle);\n }\n assertSource(absoluteSourcePath);\n } catch (error) {\n for (const lock of [...locks].reverse()) {\n try {\n releaseLock(lock.lockPath, lock.token, lock.handle);\n } catch {\n // Preserve the acquisition failure and never remove a replaced lock.\n }\n }\n if (isFileSystemError(error) && error.code === 'EEXIST') {\n throw new Error(`History ownership lock already exists or is ambiguous: ${conflictingLockPath}`, {\n cause: error,\n });\n }\n throw error;\n }\n return createLease(locks, absoluteSourcePath, assertQuiescent);\n },\n };\n}\n\n/** Explicitly named alias for hosts that want to document the v4 boundary. */\nexport const createV4HistoryOwnership = createHistoryOwnership;\n"],"mappings":"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"}
1
+ {"version":3,"file":"historyOwnership.cjs","names":["path","fs","randomUUID"],"sources":["../src/services/historyOwnership/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { HistoryOwnership, HistoryOwnershipLease } from '../historyImport';\n\nconst LOCK_SUFFIX = '.doompi-v4.lock';\nconst LOCK_VERSION = 1;\nconst LOCK_FORMAT = 'doompi-v4-history-ownership';\n\ninterface HistoryLockRecord {\n version: 1;\n format: 'doompi-v4-history-ownership';\n pid: number;\n sourcePath: string;\n token: string;\n}\n\ninterface HeldHistoryLock {\n lockPath: string;\n token: string;\n handle: number;\n}\n\nexport interface HistoryOwnershipOptions {\n /** Checks managed writers; this cannot account for an unmanaged Pi process. */\n assertQuiescent?: (sourcePath: string) => void | Promise<void>;\n /** The source format admitted by this owner. Defaults to canonical v4. */\n sourceFormat?: 'v3' | 'v4' | 'sqlite';\n /** Additional paths to lock for the lifetime of each acquired source lease. */\n additionalPaths?: readonly string[];\n}\n\nfunction canonicalSourcePath(sourcePath: string): string {\n const absolute = path.resolve(sourcePath);\n if (fs.existsSync(absolute)) return fs.realpathSync(absolute);\n const segments = [path.basename(absolute)];\n let parent = path.dirname(absolute);\n while (!fs.existsSync(parent)) {\n segments.unshift(path.basename(parent));\n parent = path.dirname(parent);\n }\n return path.join(fs.realpathSync(parent), ...segments);\n}\n\n/** Resolve aliases so every writer and exporter contends for the same lock. */\nexport function historyOwnershipLockPath(sourcePath: string): string {\n return `${canonicalSourcePath(sourcePath)}${LOCK_SUFFIX}`;\n}\n\nfunction isHistoryObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isV4Header(value: unknown): boolean {\n return isHistoryObject(value) && value.kind === 'header' && value.v === 4;\n}\n\nfunction isV3Header(value: unknown): boolean {\n return isHistoryObject(value) && value.type === 'session' && value.version === 3;\n}\n\nfunction readHeader(sourcePath: string, format: 'v3' | 'v4'): unknown {\n const firstLine = fs.readFileSync(sourcePath, 'utf8').split('\\n', 1)[0] ?? '';\n try {\n return JSON.parse(firstLine);\n } catch (error) {\n const message =\n format === 'v4'\n ? `History ownership source is not canonical v4 JSONL: ${sourcePath}`\n : `Offline history source is not canonical v3 JSONL: ${sourcePath}`;\n throw new Error(message, { cause: error });\n }\n}\n\nfunction assertIndependentRegularFile(sourcePath: string, label: string): void {\n const stat = fs.statSync(sourcePath);\n if (!stat.isFile() || stat.nlink !== 1) throw new Error(`${label} must be an independent regular file`);\n}\n\nfunction assertV4Source(sourcePath: string): void {\n if (!fs.existsSync(sourcePath)) return;\n assertIndependentRegularFile(sourcePath, 'Canonical history');\n if (!isV4Header(readHeader(sourcePath, 'v4')))\n throw new Error(`History ownership only supports canonical v4 sources: ${sourcePath}`);\n}\n\nfunction assertV3Source(sourcePath: string): void {\n if (!fs.existsSync(sourcePath)) return;\n assertIndependentRegularFile(sourcePath, 'Offline v3 history source');\n if (!isV3Header(readHeader(sourcePath, 'v3')))\n throw new Error(`Offline history import only supports v3 sources: ${sourcePath}`);\n}\n\nfunction assertSqliteSource(sourcePath: string): void {\n if (!fs.existsSync(sourcePath)) return;\n assertIndependentRegularFile(sourcePath, 'SQLite history');\n const descriptor = fs.openSync(sourcePath, 'r');\n try {\n const header = Buffer.alloc(16);\n if (\n fs.readSync(descriptor, header, 0, header.length, 0) !== header.length ||\n header.toString('utf8') !== 'SQLite format 3\\u0000'\n )\n throw new Error(`History ownership source is not SQLite: ${sourcePath}`);\n } finally {\n fs.closeSync(descriptor);\n }\n}\n\nfunction isFileSystemError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && typeof (error as NodeJS.ErrnoException).code === 'string';\n}\n\nfunction readToken(lockPath: string): string | undefined {\n try {\n const value = JSON.parse(fs.readFileSync(lockPath, 'utf8')) as Partial<HistoryLockRecord>;\n return typeof value.token === 'string' ? value.token : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** A dead owner is recoverable only when the entire lock record is recognizable. */\nfunction readRecoverableLock(lockPath: string, sourcePath: string): HistoryLockRecord | undefined {\n try {\n const stat = fs.lstatSync(lockPath);\n if (!stat.isFile() || stat.nlink !== 1) return undefined;\n const value: unknown = JSON.parse(fs.readFileSync(lockPath, 'utf8'));\n if (\n !isHistoryObject(value) ||\n value.version !== LOCK_VERSION ||\n value.format !== LOCK_FORMAT ||\n value.sourcePath !== sourcePath ||\n !Number.isSafeInteger(value.pid) ||\n (value.pid as number) <= 0 ||\n typeof value.token !== 'string' ||\n !/^[\\da-f]{8}(?:-[\\da-f]{4}){3}-[\\da-f]{12}$/iu.test(value.token)\n )\n return undefined;\n return value as unknown as HistoryLockRecord;\n } catch {\n return undefined;\n }\n}\n\nfunction ownerMayBeAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return !(isFileSystemError(error) && error.code === 'ESRCH');\n }\n}\n/** A guard left by a dead acquirer can be retired without touching its source lock. */\nfunction retireDeadGuard(guard: string): boolean {\n try {\n const stat = fs.lstatSync(guard);\n if (!stat.isDirectory()) return false;\n const entries = fs.readdirSync(guard);\n // An empty guard might still belong to a process killed between mkdir and\n // publishing its marker. Fail closed rather than race a new acquirer.\n if (entries.length !== 1 || !/^owner-[\\da-f-]{36}\\.json$/iu.test(entries[0]!)) return false;\n const marker = path.join(guard, entries[0]!);\n const markerStat = fs.lstatSync(marker);\n if (!markerStat.isFile() || markerStat.nlink !== 1) return false;\n const owner: unknown = JSON.parse(fs.readFileSync(marker, 'utf8'));\n if (!isHistoryObject(owner) || !Number.isSafeInteger(owner.pid) || (owner.pid as number) <= 0) return false;\n if (ownerMayBeAlive(owner.pid as number)) return false;\n // Only the process that unlinks this unique marker may retire the directory.\n // Other cleaners see a newly empty guard and refuse to remove it.\n fs.unlinkSync(marker);\n fs.rmdirSync(guard);\n return true;\n } catch {\n return false;\n }\n}\n\n/** All new acquirers hold this guard until their ownership record is durable. */\nfunction withAcquisitionGuard<T>(lockPath: string, acquire: () => T): T {\n const guard = `${lockPath}.recovery`;\n try {\n fs.mkdirSync(guard, { mode: 0o700 });\n } catch (error) {\n if (!isFileSystemError(error) || error.code !== 'EEXIST' || !retireDeadGuard(guard)) throw error;\n fs.mkdirSync(guard, { mode: 0o700 });\n }\n const marker = path.join(guard, `owner-${randomUUID()}.json`);\n try {\n fs.writeFileSync(marker, JSON.stringify({ pid: process.pid }), { flag: 'wx', mode: 0o600 });\n return acquire();\n } finally {\n fs.rmSync(marker, { force: true });\n fs.rmdirSync(guard);\n }\n}\n\n/** Called only under the acquisition guard, before publishing a new owner. */\nfunction recoverDeadOwner(lockPath: string, sourcePath: string): boolean {\n try {\n const record = readRecoverableLock(lockPath, sourcePath);\n if (!record || ownerMayBeAlive(record.pid)) return false;\n const before = fs.lstatSync(lockPath);\n if (readRecoverableLock(lockPath, sourcePath)?.token !== record.token || ownerMayBeAlive(record.pid)) return false;\n const current = fs.lstatSync(lockPath);\n if (before.dev !== current.dev || before.ino !== current.ino) return false;\n // The owner is gone, and other recoverers cannot remove this lock while\n // the guard is held. Legacy writers still contend on the exclusive wx open.\n fs.unlinkSync(lockPath);\n return true;\n } catch (error) {\n if (isFileSystemError(error) && error.code === 'ENOENT') return false;\n throw error;\n }\n}\n\nfunction releaseLock(lockPath: string, token: string, handle: number): void {\n fs.closeSync(handle);\n // Recovery only retires a lock after its recorded owner has exited, so that\n // owner cannot be here releasing it concurrently. A replaced lock is ambiguous.\n if (readToken(lockPath) === token) fs.rmSync(lockPath, { force: true });\n}\n\nfunction createLease(\n locks: readonly HeldHistoryLock[],\n sourcePath: string,\n assertQuiescent: (sourcePath: string) => void | Promise<void>,\n): HistoryOwnershipLease {\n let released = false;\n return {\n assertQuiescent: () => {\n if (released) throw new Error('History ownership lease has been released');\n for (const lock of locks) {\n const held = fs.fstatSync(lock.handle);\n const current = fs.statSync(lock.lockPath);\n if (held.dev !== current.dev || held.ino !== current.ino || readToken(lock.lockPath) !== lock.token) {\n throw new Error('History ownership lock was replaced');\n }\n }\n return assertQuiescent(sourcePath);\n },\n release: () => {\n if (released) return;\n released = true;\n let firstError: unknown;\n for (const lock of [...locks].reverse()) {\n try {\n releaseLock(lock.lockPath, lock.token, lock.handle);\n } catch (error) {\n firstError ??= error;\n }\n }\n if (firstError !== undefined) throw firstError;\n },\n };\n}\n\n/**\n * Creates the canonical history owner used by runtime writers, export CLI, and\n * the explicit offline importer. The default source boundary remains v4.\n * Complete locks left by a dead process are recovered under an exclusive guard.\n * A live owner or an ambiguous sidecar still blocks acquisition.\n */\nexport function createHistoryOwnership(options: HistoryOwnershipOptions = {}): HistoryOwnership {\n const assertQuiescent = options.assertQuiescent ?? (() => undefined);\n const assertSource =\n options.sourceFormat === 'sqlite'\n ? assertSqliteSource\n : options.sourceFormat === 'v3'\n ? assertV3Source\n : assertV4Source;\n return {\n async acquire(sourcePath: string): Promise<HistoryOwnershipLease> {\n const absoluteSourcePath = canonicalSourcePath(sourcePath);\n const paths = [absoluteSourcePath, ...(options.additionalPaths ?? [])].map(canonicalSourcePath);\n const canonicalPaths = [...new Set(paths)].sort((left, right) => left.localeCompare(right));\n const locks: HeldHistoryLock[] = [];\n let conflictingLockPath = historyOwnershipLockPath(absoluteSourcePath);\n try {\n for (const lockedPath of canonicalPaths) {\n const lockPath = historyOwnershipLockPath(lockedPath);\n conflictingLockPath = lockPath;\n const token = randomUUID();\n const record: HistoryLockRecord = {\n version: LOCK_VERSION,\n format: LOCK_FORMAT,\n pid: process.pid,\n sourcePath: lockedPath,\n token,\n };\n fs.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });\n withAcquisitionGuard(lockPath, () => {\n let handle: number;\n try {\n handle = fs.openSync(lockPath, 'wx', 0o600);\n } catch (error) {\n if (!isFileSystemError(error) || error.code !== 'EEXIST' || !recoverDeadOwner(lockPath, lockedPath))\n throw error;\n handle = fs.openSync(lockPath, 'wx', 0o600);\n }\n const lock = { lockPath, token, handle };\n locks.push(lock);\n fs.writeFileSync(handle, `${JSON.stringify(record)}\\n`, 'utf8');\n fs.fsyncSync(handle);\n });\n }\n assertSource(absoluteSourcePath);\n } catch (error) {\n for (const lock of [...locks].reverse()) {\n try {\n releaseLock(lock.lockPath, lock.token, lock.handle);\n } catch {\n // Preserve the acquisition failure and never remove a replaced lock.\n }\n }\n if (isFileSystemError(error) && error.code === 'EEXIST') {\n throw new Error(`History ownership lock already exists or is ambiguous: ${conflictingLockPath}`, {\n cause: error,\n });\n }\n throw error;\n }\n return createLease(locks, absoluteSourcePath, assertQuiescent);\n },\n };\n}\n\n/** Explicitly named alias for hosts that want to document the v4 boundary. */\nexport const createV4HistoryOwnership = createHistoryOwnership;\n"],"mappings":"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"}
@@ -13,8 +13,8 @@ export declare function historyOwnershipLockPath(sourcePath: string): string;
13
13
  /**
14
14
  * Creates the canonical history owner used by runtime writers, export CLI, and
15
15
  * the explicit offline importer. The default source boundary remains v4.
16
- * Locks are deliberately never reclaimed from dead pids: a stale or malformed
17
- * sidecar needs explicit operator cleanup because it cannot prove quiescence.
16
+ * Complete locks left by a dead process are recovered under an exclusive guard.
17
+ * A live owner or an ambiguous sidecar still blocks acquisition.
18
18
  */
19
19
  export declare function createHistoryOwnership(options?: HistoryOwnershipOptions): HistoryOwnership;
20
20
  /** Explicitly named alias for hosts that want to document the v4 boundary. */
@@ -1 +1 @@
1
- {"version":3,"file":"historyOwnership.d.mts","names":[],"sources":["../src/services/historyOwnership/index.ts"],"mappings":";;iBAwBiB;;EAEf,mBAAmB,8BAA8B;;EAEjD;;EAEA;;;wBAgBc,yBAAyB;;;;;;;wBA4HzB,uBAAuB,UAAS,0BAA+B;;qBAwDlE,iCAAwB"}
1
+ {"version":3,"file":"historyOwnership.d.mts","names":[],"sources":["../src/services/historyOwnership/index.ts"],"mappings":";;iBAwBiB;;EAEf,mBAAmB,8BAA8B;;EAEjD;;EAEA;;;wBAgBc,yBAAyB;;;;;;;wBA0NzB,uBAAuB,UAAS,0BAA+B;;qBAiElE,iCAAwB"}
@@ -74,6 +74,82 @@ function readToken(lockPath) {
74
74
  return;
75
75
  }
76
76
  }
77
+ /** A dead owner is recoverable only when the entire lock record is recognizable. */
78
+ function readRecoverableLock(lockPath, sourcePath) {
79
+ try {
80
+ const stat = fs.lstatSync(lockPath);
81
+ if (!stat.isFile() || stat.nlink !== 1) return void 0;
82
+ const value = JSON.parse(fs.readFileSync(lockPath, "utf8"));
83
+ if (!isHistoryObject(value) || value.version !== LOCK_VERSION || value.format !== LOCK_FORMAT || value.sourcePath !== sourcePath || !Number.isSafeInteger(value.pid) || value.pid <= 0 || typeof value.token !== "string" || !/^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/iu.test(value.token)) return void 0;
84
+ return value;
85
+ } catch {
86
+ return;
87
+ }
88
+ }
89
+ function ownerMayBeAlive(pid) {
90
+ try {
91
+ process.kill(pid, 0);
92
+ return true;
93
+ } catch (error) {
94
+ return !(isFileSystemError(error) && error.code === "ESRCH");
95
+ }
96
+ }
97
+ /** A guard left by a dead acquirer can be retired without touching its source lock. */
98
+ function retireDeadGuard(guard) {
99
+ try {
100
+ if (!fs.lstatSync(guard).isDirectory()) return false;
101
+ const entries = fs.readdirSync(guard);
102
+ if (entries.length !== 1 || !/^owner-[\da-f-]{36}\.json$/iu.test(entries[0])) return false;
103
+ const marker = path.join(guard, entries[0]);
104
+ const markerStat = fs.lstatSync(marker);
105
+ if (!markerStat.isFile() || markerStat.nlink !== 1) return false;
106
+ const owner = JSON.parse(fs.readFileSync(marker, "utf8"));
107
+ if (!isHistoryObject(owner) || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) return false;
108
+ if (ownerMayBeAlive(owner.pid)) return false;
109
+ fs.unlinkSync(marker);
110
+ fs.rmdirSync(guard);
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+ /** All new acquirers hold this guard until their ownership record is durable. */
117
+ function withAcquisitionGuard(lockPath, acquire) {
118
+ const guard = `${lockPath}.recovery`;
119
+ try {
120
+ fs.mkdirSync(guard, { mode: 448 });
121
+ } catch (error) {
122
+ if (!isFileSystemError(error) || error.code !== "EEXIST" || !retireDeadGuard(guard)) throw error;
123
+ fs.mkdirSync(guard, { mode: 448 });
124
+ }
125
+ const marker = path.join(guard, `owner-${randomUUID()}.json`);
126
+ try {
127
+ fs.writeFileSync(marker, JSON.stringify({ pid: process.pid }), {
128
+ flag: "wx",
129
+ mode: 384
130
+ });
131
+ return acquire();
132
+ } finally {
133
+ fs.rmSync(marker, { force: true });
134
+ fs.rmdirSync(guard);
135
+ }
136
+ }
137
+ /** Called only under the acquisition guard, before publishing a new owner. */
138
+ function recoverDeadOwner(lockPath, sourcePath) {
139
+ try {
140
+ const record = readRecoverableLock(lockPath, sourcePath);
141
+ if (!record || ownerMayBeAlive(record.pid)) return false;
142
+ const before = fs.lstatSync(lockPath);
143
+ if (readRecoverableLock(lockPath, sourcePath)?.token !== record.token || ownerMayBeAlive(record.pid)) return false;
144
+ const current = fs.lstatSync(lockPath);
145
+ if (before.dev !== current.dev || before.ino !== current.ino) return false;
146
+ fs.unlinkSync(lockPath);
147
+ return true;
148
+ } catch (error) {
149
+ if (isFileSystemError(error) && error.code === "ENOENT") return false;
150
+ throw error;
151
+ }
152
+ }
77
153
  function releaseLock(lockPath, token, handle) {
78
154
  fs.closeSync(handle);
79
155
  if (readToken(lockPath) === token) fs.rmSync(lockPath, { force: true });
@@ -106,8 +182,8 @@ function createLease(locks, sourcePath, assertQuiescent) {
106
182
  /**
107
183
  * Creates the canonical history owner used by runtime writers, export CLI, and
108
184
  * the explicit offline importer. The default source boundary remains v4.
109
- * Locks are deliberately never reclaimed from dead pids: a stale or malformed
110
- * sidecar needs explicit operator cleanup because it cannot prove quiescence.
185
+ * Complete locks left by a dead process are recovered under an exclusive guard.
186
+ * A live owner or an ambiguous sidecar still blocks acquisition.
111
187
  */
112
188
  function createHistoryOwnership(options = {}) {
113
189
  const assertQuiescent = options.assertQuiescent ?? (() => void 0);
@@ -134,15 +210,23 @@ function createHistoryOwnership(options = {}) {
134
210
  recursive: true,
135
211
  mode: 448
136
212
  });
137
- const handle = fs.openSync(lockPath, "wx", 384);
138
- const lock = {
139
- lockPath,
140
- token,
141
- handle
142
- };
143
- locks.push(lock);
144
- fs.writeFileSync(handle, `${JSON.stringify(record)}\n`, "utf8");
145
- fs.fsyncSync(handle);
213
+ withAcquisitionGuard(lockPath, () => {
214
+ let handle;
215
+ try {
216
+ handle = fs.openSync(lockPath, "wx", 384);
217
+ } catch (error) {
218
+ if (!isFileSystemError(error) || error.code !== "EEXIST" || !recoverDeadOwner(lockPath, lockedPath)) throw error;
219
+ handle = fs.openSync(lockPath, "wx", 384);
220
+ }
221
+ const lock = {
222
+ lockPath,
223
+ token,
224
+ handle
225
+ };
226
+ locks.push(lock);
227
+ fs.writeFileSync(handle, `${JSON.stringify(record)}\n`, "utf8");
228
+ fs.fsyncSync(handle);
229
+ });
146
230
  }
147
231
  assertSource(absoluteSourcePath);
148
232
  } catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"historyOwnership.mjs","names":[],"sources":["../src/services/historyOwnership/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { HistoryOwnership, HistoryOwnershipLease } from '../historyImport';\n\nconst LOCK_SUFFIX = '.doompi-v4.lock';\nconst LOCK_VERSION = 1;\nconst LOCK_FORMAT = 'doompi-v4-history-ownership';\n\ninterface HistoryLockRecord {\n version: 1;\n format: 'doompi-v4-history-ownership';\n pid: number;\n sourcePath: string;\n token: string;\n}\n\ninterface HeldHistoryLock {\n lockPath: string;\n token: string;\n handle: number;\n}\n\nexport interface HistoryOwnershipOptions {\n /** Checks managed writers; this cannot account for an unmanaged Pi process. */\n assertQuiescent?: (sourcePath: string) => void | Promise<void>;\n /** The source format admitted by this owner. Defaults to canonical v4. */\n sourceFormat?: 'v3' | 'v4' | 'sqlite';\n /** Additional paths to lock for the lifetime of each acquired source lease. */\n additionalPaths?: readonly string[];\n}\n\nfunction canonicalSourcePath(sourcePath: string): string {\n const absolute = path.resolve(sourcePath);\n if (fs.existsSync(absolute)) return fs.realpathSync(absolute);\n const segments = [path.basename(absolute)];\n let parent = path.dirname(absolute);\n while (!fs.existsSync(parent)) {\n segments.unshift(path.basename(parent));\n parent = path.dirname(parent);\n }\n return path.join(fs.realpathSync(parent), ...segments);\n}\n\n/** Resolve aliases so every writer and exporter contends for the same lock. */\nexport function historyOwnershipLockPath(sourcePath: string): string {\n return `${canonicalSourcePath(sourcePath)}${LOCK_SUFFIX}`;\n}\n\nfunction isHistoryObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isV4Header(value: unknown): boolean {\n return isHistoryObject(value) && value.kind === 'header' && value.v === 4;\n}\n\nfunction isV3Header(value: unknown): boolean {\n return isHistoryObject(value) && value.type === 'session' && value.version === 3;\n}\n\nfunction readHeader(sourcePath: string, format: 'v3' | 'v4'): unknown {\n const firstLine = fs.readFileSync(sourcePath, 'utf8').split('\\n', 1)[0] ?? '';\n try {\n return JSON.parse(firstLine);\n } catch (error) {\n const message =\n format === 'v4'\n ? `History ownership source is not canonical v4 JSONL: ${sourcePath}`\n : `Offline history source is not canonical v3 JSONL: ${sourcePath}`;\n throw new Error(message, { cause: error });\n }\n}\n\nfunction assertIndependentRegularFile(sourcePath: string, label: string): void {\n const stat = fs.statSync(sourcePath);\n if (!stat.isFile() || stat.nlink !== 1) throw new Error(`${label} must be an independent regular file`);\n}\n\nfunction assertV4Source(sourcePath: string): void {\n if (!fs.existsSync(sourcePath)) return;\n assertIndependentRegularFile(sourcePath, 'Canonical history');\n if (!isV4Header(readHeader(sourcePath, 'v4')))\n throw new Error(`History ownership only supports canonical v4 sources: ${sourcePath}`);\n}\n\nfunction assertV3Source(sourcePath: string): void {\n if (!fs.existsSync(sourcePath)) return;\n assertIndependentRegularFile(sourcePath, 'Offline v3 history source');\n if (!isV3Header(readHeader(sourcePath, 'v3')))\n throw new Error(`Offline history import only supports v3 sources: ${sourcePath}`);\n}\n\nfunction assertSqliteSource(sourcePath: string): void {\n if (!fs.existsSync(sourcePath)) return;\n assertIndependentRegularFile(sourcePath, 'SQLite history');\n const descriptor = fs.openSync(sourcePath, 'r');\n try {\n const header = Buffer.alloc(16);\n if (\n fs.readSync(descriptor, header, 0, header.length, 0) !== header.length ||\n header.toString('utf8') !== 'SQLite format 3\\u0000'\n )\n throw new Error(`History ownership source is not SQLite: ${sourcePath}`);\n } finally {\n fs.closeSync(descriptor);\n }\n}\n\nfunction isFileSystemError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && typeof (error as NodeJS.ErrnoException).code === 'string';\n}\n\nfunction readToken(lockPath: string): string | undefined {\n try {\n const value = JSON.parse(fs.readFileSync(lockPath, 'utf8')) as Partial<HistoryLockRecord>;\n return typeof value.token === 'string' ? value.token : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction releaseLock(lockPath: string, token: string, handle: number): void {\n fs.closeSync(handle);\n // An unrecognisable or replaced lock is ambiguous. Never remove it merely\n // because its recorded pid is gone or its contents look stale.\n if (readToken(lockPath) === token) fs.rmSync(lockPath, { force: true });\n}\n\nfunction createLease(\n locks: readonly HeldHistoryLock[],\n sourcePath: string,\n assertQuiescent: (sourcePath: string) => void | Promise<void>,\n): HistoryOwnershipLease {\n let released = false;\n return {\n assertQuiescent: () => {\n if (released) throw new Error('History ownership lease has been released');\n for (const lock of locks) {\n const held = fs.fstatSync(lock.handle);\n const current = fs.statSync(lock.lockPath);\n if (held.dev !== current.dev || held.ino !== current.ino || readToken(lock.lockPath) !== lock.token) {\n throw new Error('History ownership lock was replaced');\n }\n }\n return assertQuiescent(sourcePath);\n },\n release: () => {\n if (released) return;\n released = true;\n let firstError: unknown;\n for (const lock of [...locks].reverse()) {\n try {\n releaseLock(lock.lockPath, lock.token, lock.handle);\n } catch (error) {\n firstError ??= error;\n }\n }\n if (firstError !== undefined) throw firstError;\n },\n };\n}\n\n/**\n * Creates the canonical history owner used by runtime writers, export CLI, and\n * the explicit offline importer. The default source boundary remains v4.\n * Locks are deliberately never reclaimed from dead pids: a stale or malformed\n * sidecar needs explicit operator cleanup because it cannot prove quiescence.\n */\nexport function createHistoryOwnership(options: HistoryOwnershipOptions = {}): HistoryOwnership {\n const assertQuiescent = options.assertQuiescent ?? (() => undefined);\n const assertSource =\n options.sourceFormat === 'sqlite'\n ? assertSqliteSource\n : options.sourceFormat === 'v3'\n ? assertV3Source\n : assertV4Source;\n return {\n async acquire(sourcePath: string): Promise<HistoryOwnershipLease> {\n const absoluteSourcePath = canonicalSourcePath(sourcePath);\n const paths = [absoluteSourcePath, ...(options.additionalPaths ?? [])].map(canonicalSourcePath);\n const canonicalPaths = [...new Set(paths)].sort((left, right) => left.localeCompare(right));\n const locks: HeldHistoryLock[] = [];\n let conflictingLockPath = historyOwnershipLockPath(absoluteSourcePath);\n try {\n for (const lockedPath of canonicalPaths) {\n const lockPath = historyOwnershipLockPath(lockedPath);\n conflictingLockPath = lockPath;\n const token = randomUUID();\n const record: HistoryLockRecord = {\n version: LOCK_VERSION,\n format: LOCK_FORMAT,\n pid: process.pid,\n sourcePath: lockedPath,\n token,\n };\n fs.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });\n const handle = fs.openSync(lockPath, 'wx', 0o600);\n const lock = { lockPath, token, handle };\n locks.push(lock);\n fs.writeFileSync(handle, `${JSON.stringify(record)}\\n`, 'utf8');\n fs.fsyncSync(handle);\n }\n assertSource(absoluteSourcePath);\n } catch (error) {\n for (const lock of [...locks].reverse()) {\n try {\n releaseLock(lock.lockPath, lock.token, lock.handle);\n } catch {\n // Preserve the acquisition failure and never remove a replaced lock.\n }\n }\n if (isFileSystemError(error) && error.code === 'EEXIST') {\n throw new Error(`History ownership lock already exists or is ambiguous: ${conflictingLockPath}`, {\n cause: error,\n });\n }\n throw error;\n }\n return createLease(locks, absoluteSourcePath, assertQuiescent);\n },\n };\n}\n\n/** Explicitly named alias for hosts that want to document the v4 boundary. */\nexport const createV4HistoryOwnership = createHistoryOwnership;\n"],"mappings":";;;;AAMA,MAAM,cAAc;AACpB,MAAM,eAAe;AACrB,MAAM,cAAc;AAyBpB,SAAS,oBAAoB,YAA4B;CACvD,MAAM,WAAW,KAAK,QAAQ,UAAU;CACxC,IAAI,GAAG,WAAW,QAAQ,GAAG,OAAO,GAAG,aAAa,QAAQ;CAC5D,MAAM,WAAW,CAAC,KAAK,SAAS,QAAQ,CAAC;CACzC,IAAI,SAAS,KAAK,QAAQ,QAAQ;CAClC,OAAO,CAAC,GAAG,WAAW,MAAM,GAAG;EAC7B,SAAS,QAAQ,KAAK,SAAS,MAAM,CAAC;EACtC,SAAS,KAAK,QAAQ,MAAM;CAC9B;CACA,OAAO,KAAK,KAAK,GAAG,aAAa,MAAM,GAAG,GAAG,QAAQ;AACvD;;AAGA,SAAgB,yBAAyB,YAA4B;CACnE,OAAO,GAAG,oBAAoB,UAAU,IAAI;AAC9C;AAEA,SAAS,gBAAgB,OAAkD;CACzE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,WAAW,OAAyB;CAC3C,OAAO,gBAAgB,KAAK,KAAK,MAAM,SAAS,YAAY,MAAM,MAAM;AAC1E;AAEA,SAAS,WAAW,OAAyB;CAC3C,OAAO,gBAAgB,KAAK,KAAK,MAAM,SAAS,aAAa,MAAM,YAAY;AACjF;AAEA,SAAS,WAAW,YAAoB,QAA8B;CACpE,MAAM,YAAY,GAAG,aAAa,YAAY,MAAM,CAAC,CAAC,MAAM,MAAM,CAAC,CAAC,CAAC,MAAM;CAC3E,IAAI;EACF,OAAO,KAAK,MAAM,SAAS;CAC7B,SAAS,OAAO;EACd,MAAM,UACJ,WAAW,OACP,uDAAuD,eACvD,qDAAqD;EAC3D,MAAM,IAAI,MAAM,SAAS,EAAE,OAAO,MAAM,CAAC;CAC3C;AACF;AAEA,SAAS,6BAA6B,YAAoB,OAAqB;CAC7E,MAAM,OAAO,GAAG,SAAS,UAAU;CACnC,IAAI,CAAC,KAAK,OAAO,KAAK,KAAK,UAAU,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,qCAAqC;AACxG;AAEA,SAAS,eAAe,YAA0B;CAChD,IAAI,CAAC,GAAG,WAAW,UAAU,GAAG;CAChC,6BAA6B,YAAY,mBAAmB;CAC5D,IAAI,CAAC,WAAW,WAAW,YAAY,IAAI,CAAC,GAC1C,MAAM,IAAI,MAAM,yDAAyD,YAAY;AACzF;AAEA,SAAS,eAAe,YAA0B;CAChD,IAAI,CAAC,GAAG,WAAW,UAAU,GAAG;CAChC,6BAA6B,YAAY,2BAA2B;CACpE,IAAI,CAAC,WAAW,WAAW,YAAY,IAAI,CAAC,GAC1C,MAAM,IAAI,MAAM,oDAAoD,YAAY;AACpF;AAEA,SAAS,mBAAmB,YAA0B;CACpD,IAAI,CAAC,GAAG,WAAW,UAAU,GAAG;CAChC,6BAA6B,YAAY,gBAAgB;CACzD,MAAM,aAAa,GAAG,SAAS,YAAY,GAAG;CAC9C,IAAI;EACF,MAAM,SAAS,OAAO,MAAM,EAAE;EAC9B,IACE,GAAG,SAAS,YAAY,QAAQ,GAAG,OAAO,QAAQ,CAAC,MAAM,OAAO,UAChE,OAAO,SAAS,MAAM,MAAM,qBAE5B,MAAM,IAAI,MAAM,2CAA2C,YAAY;CAC3E,UAAU;EACR,GAAG,UAAU,UAAU;CACzB;AACF;AAEA,SAAS,kBAAkB,OAAgD;CACzE,OAAO,iBAAiB,SAAS,OAAQ,MAAgC,SAAS;AACpF;AAEA,SAAS,UAAU,UAAsC;CACvD,IAAI;EACF,MAAM,QAAQ,KAAK,MAAM,GAAG,aAAa,UAAU,MAAM,CAAC;EAC1D,OAAO,OAAO,MAAM,UAAU,WAAW,MAAM,QAAQ,KAAA;CACzD,QAAQ;EACN;CACF;AACF;AAEA,SAAS,YAAY,UAAkB,OAAe,QAAsB;CAC1E,GAAG,UAAU,MAAM;CAGnB,IAAI,UAAU,QAAQ,MAAM,OAAO,GAAG,OAAO,UAAU,EAAE,OAAO,KAAK,CAAC;AACxE;AAEA,SAAS,YACP,OACA,YACA,iBACuB;CACvB,IAAI,WAAW;CACf,OAAO;EACL,uBAAuB;GACrB,IAAI,UAAU,MAAM,IAAI,MAAM,2CAA2C;GACzE,KAAK,MAAM,QAAQ,OAAO;IACxB,MAAM,OAAO,GAAG,UAAU,KAAK,MAAM;IACrC,MAAM,UAAU,GAAG,SAAS,KAAK,QAAQ;IACzC,IAAI,KAAK,QAAQ,QAAQ,OAAO,KAAK,QAAQ,QAAQ,OAAO,UAAU,KAAK,QAAQ,MAAM,KAAK,OAC5F,MAAM,IAAI,MAAM,qCAAqC;GAEzD;GACA,OAAO,gBAAgB,UAAU;EACnC;EACA,eAAe;GACb,IAAI,UAAU;GACd,WAAW;GACX,IAAI;GACJ,KAAK,MAAM,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,QAAQ,GACpC,IAAI;IACF,YAAY,KAAK,UAAU,KAAK,OAAO,KAAK,MAAM;GACpD,SAAS,OAAO;IACd,eAAe;GACjB;GAEF,IAAI,eAAe,KAAA,GAAW,MAAM;EACtC;CACF;AACF;;;;;;;AAQA,SAAgB,uBAAuB,UAAmC,CAAC,GAAqB;CAC9F,MAAM,kBAAkB,QAAQ,0BAA0B,KAAA;CAC1D,MAAM,eACJ,QAAQ,iBAAiB,WACrB,qBACA,QAAQ,iBAAiB,OACvB,iBACA;CACR,OAAO,EACL,MAAM,QAAQ,YAAoD;EAChE,MAAM,qBAAqB,oBAAoB,UAAU;EACzD,MAAM,QAAQ,CAAC,oBAAoB,GAAI,QAAQ,mBAAmB,CAAC,CAAE,CAAC,CAAC,IAAI,mBAAmB;EAC9F,MAAM,iBAAiB,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,MAAM,MAAM,UAAU,KAAK,cAAc,KAAK,CAAC;EAC1F,MAAM,QAA2B,CAAC;EAClC,IAAI,sBAAsB,yBAAyB,kBAAkB;EACrE,IAAI;GACF,KAAK,MAAM,cAAc,gBAAgB;IACvC,MAAM,WAAW,yBAAyB,UAAU;IACpD,sBAAsB;IACtB,MAAM,QAAQ,WAAW;IACzB,MAAM,SAA4B;KAChC,SAAS;KACT,QAAQ;KACR,KAAK,QAAQ;KACb,YAAY;KACZ;IACF;IACA,GAAG,UAAU,KAAK,QAAQ,QAAQ,GAAG;KAAE,WAAW;KAAM,MAAM;IAAM,CAAC;IACrE,MAAM,SAAS,GAAG,SAAS,UAAU,MAAM,GAAK;IAChD,MAAM,OAAO;KAAE;KAAU;KAAO;IAAO;IACvC,MAAM,KAAK,IAAI;IACf,GAAG,cAAc,QAAQ,GAAG,KAAK,UAAU,MAAM,EAAE,KAAK,MAAM;IAC9D,GAAG,UAAU,MAAM;GACrB;GACA,aAAa,kBAAkB;EACjC,SAAS,OAAO;GACd,KAAK,MAAM,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,QAAQ,GACpC,IAAI;IACF,YAAY,KAAK,UAAU,KAAK,OAAO,KAAK,MAAM;GACpD,QAAQ,CAER;GAEF,IAAI,kBAAkB,KAAK,KAAK,MAAM,SAAS,UAC7C,MAAM,IAAI,MAAM,0DAA0D,uBAAuB,EAC/F,OAAO,MACT,CAAC;GAEH,MAAM;EACR;EACA,OAAO,YAAY,OAAO,oBAAoB,eAAe;CAC/D,EACF;AACF;;AAGA,MAAa,2BAA2B"}
1
+ {"version":3,"file":"historyOwnership.mjs","names":[],"sources":["../src/services/historyOwnership/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { HistoryOwnership, HistoryOwnershipLease } from '../historyImport';\n\nconst LOCK_SUFFIX = '.doompi-v4.lock';\nconst LOCK_VERSION = 1;\nconst LOCK_FORMAT = 'doompi-v4-history-ownership';\n\ninterface HistoryLockRecord {\n version: 1;\n format: 'doompi-v4-history-ownership';\n pid: number;\n sourcePath: string;\n token: string;\n}\n\ninterface HeldHistoryLock {\n lockPath: string;\n token: string;\n handle: number;\n}\n\nexport interface HistoryOwnershipOptions {\n /** Checks managed writers; this cannot account for an unmanaged Pi process. */\n assertQuiescent?: (sourcePath: string) => void | Promise<void>;\n /** The source format admitted by this owner. Defaults to canonical v4. */\n sourceFormat?: 'v3' | 'v4' | 'sqlite';\n /** Additional paths to lock for the lifetime of each acquired source lease. */\n additionalPaths?: readonly string[];\n}\n\nfunction canonicalSourcePath(sourcePath: string): string {\n const absolute = path.resolve(sourcePath);\n if (fs.existsSync(absolute)) return fs.realpathSync(absolute);\n const segments = [path.basename(absolute)];\n let parent = path.dirname(absolute);\n while (!fs.existsSync(parent)) {\n segments.unshift(path.basename(parent));\n parent = path.dirname(parent);\n }\n return path.join(fs.realpathSync(parent), ...segments);\n}\n\n/** Resolve aliases so every writer and exporter contends for the same lock. */\nexport function historyOwnershipLockPath(sourcePath: string): string {\n return `${canonicalSourcePath(sourcePath)}${LOCK_SUFFIX}`;\n}\n\nfunction isHistoryObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isV4Header(value: unknown): boolean {\n return isHistoryObject(value) && value.kind === 'header' && value.v === 4;\n}\n\nfunction isV3Header(value: unknown): boolean {\n return isHistoryObject(value) && value.type === 'session' && value.version === 3;\n}\n\nfunction readHeader(sourcePath: string, format: 'v3' | 'v4'): unknown {\n const firstLine = fs.readFileSync(sourcePath, 'utf8').split('\\n', 1)[0] ?? '';\n try {\n return JSON.parse(firstLine);\n } catch (error) {\n const message =\n format === 'v4'\n ? `History ownership source is not canonical v4 JSONL: ${sourcePath}`\n : `Offline history source is not canonical v3 JSONL: ${sourcePath}`;\n throw new Error(message, { cause: error });\n }\n}\n\nfunction assertIndependentRegularFile(sourcePath: string, label: string): void {\n const stat = fs.statSync(sourcePath);\n if (!stat.isFile() || stat.nlink !== 1) throw new Error(`${label} must be an independent regular file`);\n}\n\nfunction assertV4Source(sourcePath: string): void {\n if (!fs.existsSync(sourcePath)) return;\n assertIndependentRegularFile(sourcePath, 'Canonical history');\n if (!isV4Header(readHeader(sourcePath, 'v4')))\n throw new Error(`History ownership only supports canonical v4 sources: ${sourcePath}`);\n}\n\nfunction assertV3Source(sourcePath: string): void {\n if (!fs.existsSync(sourcePath)) return;\n assertIndependentRegularFile(sourcePath, 'Offline v3 history source');\n if (!isV3Header(readHeader(sourcePath, 'v3')))\n throw new Error(`Offline history import only supports v3 sources: ${sourcePath}`);\n}\n\nfunction assertSqliteSource(sourcePath: string): void {\n if (!fs.existsSync(sourcePath)) return;\n assertIndependentRegularFile(sourcePath, 'SQLite history');\n const descriptor = fs.openSync(sourcePath, 'r');\n try {\n const header = Buffer.alloc(16);\n if (\n fs.readSync(descriptor, header, 0, header.length, 0) !== header.length ||\n header.toString('utf8') !== 'SQLite format 3\\u0000'\n )\n throw new Error(`History ownership source is not SQLite: ${sourcePath}`);\n } finally {\n fs.closeSync(descriptor);\n }\n}\n\nfunction isFileSystemError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && typeof (error as NodeJS.ErrnoException).code === 'string';\n}\n\nfunction readToken(lockPath: string): string | undefined {\n try {\n const value = JSON.parse(fs.readFileSync(lockPath, 'utf8')) as Partial<HistoryLockRecord>;\n return typeof value.token === 'string' ? value.token : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** A dead owner is recoverable only when the entire lock record is recognizable. */\nfunction readRecoverableLock(lockPath: string, sourcePath: string): HistoryLockRecord | undefined {\n try {\n const stat = fs.lstatSync(lockPath);\n if (!stat.isFile() || stat.nlink !== 1) return undefined;\n const value: unknown = JSON.parse(fs.readFileSync(lockPath, 'utf8'));\n if (\n !isHistoryObject(value) ||\n value.version !== LOCK_VERSION ||\n value.format !== LOCK_FORMAT ||\n value.sourcePath !== sourcePath ||\n !Number.isSafeInteger(value.pid) ||\n (value.pid as number) <= 0 ||\n typeof value.token !== 'string' ||\n !/^[\\da-f]{8}(?:-[\\da-f]{4}){3}-[\\da-f]{12}$/iu.test(value.token)\n )\n return undefined;\n return value as unknown as HistoryLockRecord;\n } catch {\n return undefined;\n }\n}\n\nfunction ownerMayBeAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return !(isFileSystemError(error) && error.code === 'ESRCH');\n }\n}\n/** A guard left by a dead acquirer can be retired without touching its source lock. */\nfunction retireDeadGuard(guard: string): boolean {\n try {\n const stat = fs.lstatSync(guard);\n if (!stat.isDirectory()) return false;\n const entries = fs.readdirSync(guard);\n // An empty guard might still belong to a process killed between mkdir and\n // publishing its marker. Fail closed rather than race a new acquirer.\n if (entries.length !== 1 || !/^owner-[\\da-f-]{36}\\.json$/iu.test(entries[0]!)) return false;\n const marker = path.join(guard, entries[0]!);\n const markerStat = fs.lstatSync(marker);\n if (!markerStat.isFile() || markerStat.nlink !== 1) return false;\n const owner: unknown = JSON.parse(fs.readFileSync(marker, 'utf8'));\n if (!isHistoryObject(owner) || !Number.isSafeInteger(owner.pid) || (owner.pid as number) <= 0) return false;\n if (ownerMayBeAlive(owner.pid as number)) return false;\n // Only the process that unlinks this unique marker may retire the directory.\n // Other cleaners see a newly empty guard and refuse to remove it.\n fs.unlinkSync(marker);\n fs.rmdirSync(guard);\n return true;\n } catch {\n return false;\n }\n}\n\n/** All new acquirers hold this guard until their ownership record is durable. */\nfunction withAcquisitionGuard<T>(lockPath: string, acquire: () => T): T {\n const guard = `${lockPath}.recovery`;\n try {\n fs.mkdirSync(guard, { mode: 0o700 });\n } catch (error) {\n if (!isFileSystemError(error) || error.code !== 'EEXIST' || !retireDeadGuard(guard)) throw error;\n fs.mkdirSync(guard, { mode: 0o700 });\n }\n const marker = path.join(guard, `owner-${randomUUID()}.json`);\n try {\n fs.writeFileSync(marker, JSON.stringify({ pid: process.pid }), { flag: 'wx', mode: 0o600 });\n return acquire();\n } finally {\n fs.rmSync(marker, { force: true });\n fs.rmdirSync(guard);\n }\n}\n\n/** Called only under the acquisition guard, before publishing a new owner. */\nfunction recoverDeadOwner(lockPath: string, sourcePath: string): boolean {\n try {\n const record = readRecoverableLock(lockPath, sourcePath);\n if (!record || ownerMayBeAlive(record.pid)) return false;\n const before = fs.lstatSync(lockPath);\n if (readRecoverableLock(lockPath, sourcePath)?.token !== record.token || ownerMayBeAlive(record.pid)) return false;\n const current = fs.lstatSync(lockPath);\n if (before.dev !== current.dev || before.ino !== current.ino) return false;\n // The owner is gone, and other recoverers cannot remove this lock while\n // the guard is held. Legacy writers still contend on the exclusive wx open.\n fs.unlinkSync(lockPath);\n return true;\n } catch (error) {\n if (isFileSystemError(error) && error.code === 'ENOENT') return false;\n throw error;\n }\n}\n\nfunction releaseLock(lockPath: string, token: string, handle: number): void {\n fs.closeSync(handle);\n // Recovery only retires a lock after its recorded owner has exited, so that\n // owner cannot be here releasing it concurrently. A replaced lock is ambiguous.\n if (readToken(lockPath) === token) fs.rmSync(lockPath, { force: true });\n}\n\nfunction createLease(\n locks: readonly HeldHistoryLock[],\n sourcePath: string,\n assertQuiescent: (sourcePath: string) => void | Promise<void>,\n): HistoryOwnershipLease {\n let released = false;\n return {\n assertQuiescent: () => {\n if (released) throw new Error('History ownership lease has been released');\n for (const lock of locks) {\n const held = fs.fstatSync(lock.handle);\n const current = fs.statSync(lock.lockPath);\n if (held.dev !== current.dev || held.ino !== current.ino || readToken(lock.lockPath) !== lock.token) {\n throw new Error('History ownership lock was replaced');\n }\n }\n return assertQuiescent(sourcePath);\n },\n release: () => {\n if (released) return;\n released = true;\n let firstError: unknown;\n for (const lock of [...locks].reverse()) {\n try {\n releaseLock(lock.lockPath, lock.token, lock.handle);\n } catch (error) {\n firstError ??= error;\n }\n }\n if (firstError !== undefined) throw firstError;\n },\n };\n}\n\n/**\n * Creates the canonical history owner used by runtime writers, export CLI, and\n * the explicit offline importer. The default source boundary remains v4.\n * Complete locks left by a dead process are recovered under an exclusive guard.\n * A live owner or an ambiguous sidecar still blocks acquisition.\n */\nexport function createHistoryOwnership(options: HistoryOwnershipOptions = {}): HistoryOwnership {\n const assertQuiescent = options.assertQuiescent ?? (() => undefined);\n const assertSource =\n options.sourceFormat === 'sqlite'\n ? assertSqliteSource\n : options.sourceFormat === 'v3'\n ? assertV3Source\n : assertV4Source;\n return {\n async acquire(sourcePath: string): Promise<HistoryOwnershipLease> {\n const absoluteSourcePath = canonicalSourcePath(sourcePath);\n const paths = [absoluteSourcePath, ...(options.additionalPaths ?? [])].map(canonicalSourcePath);\n const canonicalPaths = [...new Set(paths)].sort((left, right) => left.localeCompare(right));\n const locks: HeldHistoryLock[] = [];\n let conflictingLockPath = historyOwnershipLockPath(absoluteSourcePath);\n try {\n for (const lockedPath of canonicalPaths) {\n const lockPath = historyOwnershipLockPath(lockedPath);\n conflictingLockPath = lockPath;\n const token = randomUUID();\n const record: HistoryLockRecord = {\n version: LOCK_VERSION,\n format: LOCK_FORMAT,\n pid: process.pid,\n sourcePath: lockedPath,\n token,\n };\n fs.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });\n withAcquisitionGuard(lockPath, () => {\n let handle: number;\n try {\n handle = fs.openSync(lockPath, 'wx', 0o600);\n } catch (error) {\n if (!isFileSystemError(error) || error.code !== 'EEXIST' || !recoverDeadOwner(lockPath, lockedPath))\n throw error;\n handle = fs.openSync(lockPath, 'wx', 0o600);\n }\n const lock = { lockPath, token, handle };\n locks.push(lock);\n fs.writeFileSync(handle, `${JSON.stringify(record)}\\n`, 'utf8');\n fs.fsyncSync(handle);\n });\n }\n assertSource(absoluteSourcePath);\n } catch (error) {\n for (const lock of [...locks].reverse()) {\n try {\n releaseLock(lock.lockPath, lock.token, lock.handle);\n } catch {\n // Preserve the acquisition failure and never remove a replaced lock.\n }\n }\n if (isFileSystemError(error) && error.code === 'EEXIST') {\n throw new Error(`History ownership lock already exists or is ambiguous: ${conflictingLockPath}`, {\n cause: error,\n });\n }\n throw error;\n }\n return createLease(locks, absoluteSourcePath, assertQuiescent);\n },\n };\n}\n\n/** Explicitly named alias for hosts that want to document the v4 boundary. */\nexport const createV4HistoryOwnership = createHistoryOwnership;\n"],"mappings":"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"}
@@ -1123,7 +1123,8 @@ async function createDirectHarnessRuntime(options) {
1123
1123
  }
1124
1124
  };
1125
1125
  const admitPrompt = async (text, images, streamingBehavior) => {
1126
- if ((await lane.inspectExecution(context)).current !== null && streamingBehavior !== void 0) {
1126
+ const execution = await lane.inspectExecution(context);
1127
+ if (execution.current !== null && streamingBehavior !== void 0) {
1127
1128
  const queued = streamingBehavior === "steer" ? await writable(() => lane.steer(text, images, context)) : await writable(() => lane.followUp(text, images, context));
1128
1129
  if (!queued.ok) resultError(queued);
1129
1130
  await adoptNativeQueue();
@@ -1138,7 +1139,22 @@ async function createDirectHarnessRuntime(options) {
1138
1139
  prompt: text
1139
1140
  };
1140
1141
  const admission = await serializeAdmission(async () => {
1141
- if ((await readRecord()).paused) throw new Error("The agent queue is paused; resume before starting a turn");
1142
+ const lifecycle = await readRecord();
1143
+ if (lifecycle.paused) {
1144
+ if (execution.current !== null || settlingOperationId !== void 0 || lifecycle.queue.some((entry) => entry.disposition !== "consumed" && entry.disposition !== "removed")) throw new Error("The agent queue is paused; resume before starting a turn");
1145
+ await changeRecord((record) => {
1146
+ if (record.queue.some((entry) => entry.disposition !== "consumed" && entry.disposition !== "removed")) throw new Error("The agent queue is paused; resume before starting a turn");
1147
+ return {
1148
+ record: {
1149
+ ...record,
1150
+ paused: false,
1151
+ abortOperationId: void 0
1152
+ },
1153
+ result: void 0
1154
+ };
1155
+ });
1156
+ await publishLifecycle();
1157
+ }
1142
1158
  for (const item of (await readRecord()).queue.filter((entry) => entry.disposition === "pending" && entry.scheduling === "held")) {
1143
1159
  await changeRecord((record) => ({
1144
1160
  record: {
@@ -1724,8 +1740,10 @@ async function createDirectHarnessRuntime(options) {
1724
1740
  thinkingLevel: thinking,
1725
1741
  isStreaming: lifecycle.operation?.kind === "run",
1726
1742
  isCompacting: lifecycle.operation?.kind === "compaction",
1727
- operationId: lifecycle.operation?.id,
1728
- executionStatus: lifecycle.operation?.status ?? "idle",
1743
+ ...lifecycle.operation === null ? {} : {
1744
+ operationId: lifecycle.operation.id,
1745
+ executionStatus: lifecycle.operation.status
1746
+ },
1729
1747
  queuePaused: lifecycle.paused,
1730
1748
  queueRevision: lifecycle.revision,
1731
1749
  steeringMode: await harness.getSteeringMode(context),