thumbgate 1.31.0 → 1.34.0
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.
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +40 -3
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +21 -0
- package/config/gates/claim-verifiers.example.json +42 -0
- package/config/gates/claim-verifiers.json +25 -0
- package/config/mcp-allowlists.json +21 -0
- package/hooks/hooks.json +1 -1
- package/package.json +14 -9
- package/public/index.html +2 -2
- package/public/numbers.html +2 -2
- package/scripts/agent-readiness.js +110 -0
- package/scripts/auto-wire-hooks.js +20 -8
- package/scripts/cli-schema.js +14 -0
- package/scripts/feedback-schema.js +3 -0
- package/scripts/file-ledger-lock.js +130 -0
- package/scripts/financial-control-plane.js +1514 -0
- package/scripts/gates-engine.js +185 -7
- package/scripts/gemini-embedding-policy.js +1 -0
- package/scripts/hook-runtime.js +5 -0
- package/scripts/hook-stop-anti-claim.js +63 -3
- package/scripts/human-escalation.js +353 -41
- package/scripts/provider-action-normalizer.js +11 -4
- package/scripts/tool-registry.js +95 -5
- package/scripts/universal-claim-evaluator.js +767 -0
- package/scripts/vector-store.js +60 -27
- package/scripts/workflow-sentinel.js +77 -11
- package/server.json +2 -2
- package/src/api/server.js +2 -0
|
@@ -165,6 +165,95 @@ function summarizePermissionTier(profileName = getActiveMcpProfile()) {
|
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
|
|
169
|
+
function detectStopHookRegistered(projectRoot, existsSync, readFileSync) {
|
|
170
|
+
try {
|
|
171
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
172
|
+
if (!existsSync(settingsPath)) return false;
|
|
173
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
174
|
+
const stopHooks = settings?.hooks?.Stop || [];
|
|
175
|
+
const flat = Array.isArray(stopHooks)
|
|
176
|
+
? stopHooks.flatMap((entry) => entry?.hooks || [entry])
|
|
177
|
+
: [];
|
|
178
|
+
return flat.some((hook) => String(hook?.command || '').includes('hook-stop-anti-claim'));
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function recommendationForClaimState({
|
|
185
|
+
evaluatorReady,
|
|
186
|
+
configLoadFailed,
|
|
187
|
+
verifierCount,
|
|
188
|
+
stopHookRegistered,
|
|
189
|
+
configSource,
|
|
190
|
+
loadErrorMessage,
|
|
191
|
+
}) {
|
|
192
|
+
if (!evaluatorReady) {
|
|
193
|
+
return 'Universal claim evaluator module is missing from this install.';
|
|
194
|
+
}
|
|
195
|
+
if (configLoadFailed) {
|
|
196
|
+
return loadErrorMessage;
|
|
197
|
+
}
|
|
198
|
+
if (verifierCount === 0) {
|
|
199
|
+
return 'No claim verifiers configured. Copy config/gates/claim-verifiers.example.json to .thumbgate/claim-verifiers.json and point subjects at your sources of truth.';
|
|
200
|
+
}
|
|
201
|
+
if (!stopHookRegistered) {
|
|
202
|
+
return 'Claim verifiers are present, but the Claude Stop anti-claim hook is not registered in .claude/settings.json.';
|
|
203
|
+
}
|
|
204
|
+
return `Factual claim recheck is ready (${verifierCount} verifier(s) from ${configSource}).`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function summarizeClaimVerification(projectRoot = PROJECT_ROOT, deps = {}) {
|
|
208
|
+
const resolveEvaluator = deps.resolveEvaluator
|
|
209
|
+
|| (() => require.resolve('./universal-claim-evaluator'));
|
|
210
|
+
const loadVerifierConfig = deps.loadVerifierConfig
|
|
211
|
+
|| (() => require('./universal-claim-evaluator').loadVerifierConfig);
|
|
212
|
+
const readFileSync = deps.readFileSync || fs.readFileSync;
|
|
213
|
+
const existsSync = deps.existsSync || fs.existsSync;
|
|
214
|
+
|
|
215
|
+
let evaluatorReady = false;
|
|
216
|
+
try {
|
|
217
|
+
resolveEvaluator();
|
|
218
|
+
evaluatorReady = true;
|
|
219
|
+
} catch {
|
|
220
|
+
evaluatorReady = false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let verifierCount = 0;
|
|
224
|
+
let configSource = 'none';
|
|
225
|
+
let configLoadFailed = false;
|
|
226
|
+
let loadErrorMessage = 'Install ThumbGate and configure claim verifiers under .thumbgate/claim-verifiers.json.';
|
|
227
|
+
try {
|
|
228
|
+
const loaded = loadVerifierConfig()({ cwd: projectRoot });
|
|
229
|
+
verifierCount = Array.isArray(loaded.verifiers) ? loaded.verifiers.length : 0;
|
|
230
|
+
configSource = loaded.source || 'none';
|
|
231
|
+
} catch (error) {
|
|
232
|
+
configLoadFailed = true;
|
|
233
|
+
loadErrorMessage = `Claim verifier config failed to load: ${error?.message || 'unknown error'}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const stopHookRegistered = detectStopHookRegistered(projectRoot, existsSync, readFileSync);
|
|
237
|
+
const recommendation = recommendationForClaimState({
|
|
238
|
+
evaluatorReady,
|
|
239
|
+
configLoadFailed,
|
|
240
|
+
verifierCount,
|
|
241
|
+
stopHookRegistered,
|
|
242
|
+
configSource,
|
|
243
|
+
loadErrorMessage,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
ready: evaluatorReady && verifierCount > 0 && stopHookRegistered && !configLoadFailed,
|
|
248
|
+
evaluatorReady,
|
|
249
|
+
verifierCount,
|
|
250
|
+
configSource,
|
|
251
|
+
stopHookRegistered,
|
|
252
|
+
configLoadFailed,
|
|
253
|
+
recommendation,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
168
257
|
function generateAgentReadinessReport({
|
|
169
258
|
projectRoot = PROJECT_ROOT,
|
|
170
259
|
mcpProfile = null,
|
|
@@ -172,11 +261,18 @@ function generateAgentReadinessReport({
|
|
|
172
261
|
const runtime = detectRuntimeIsolation();
|
|
173
262
|
const bootstrap = collectBootstrapFiles(projectRoot);
|
|
174
263
|
const permissions = summarizePermissionTier(mcpProfile || getActiveMcpProfile());
|
|
264
|
+
const claimVerification = summarizeClaimVerification(projectRoot);
|
|
175
265
|
|
|
176
266
|
const warnings = [];
|
|
177
267
|
if (!runtime.isolated) warnings.push(runtime.recommendation);
|
|
178
268
|
if (!bootstrap.ready) warnings.push(bootstrap.recommendation);
|
|
179
269
|
if (!permissions.ready) warnings.push(permissions.recommendation);
|
|
270
|
+
// Missing operator verifiers is advisory (not every project asserts SQL row counts).
|
|
271
|
+
// A missing evaluator module or a broken claim-verifier config is not advisory —
|
|
272
|
+
// both make factual recheck fail closed at runtime and must surface here.
|
|
273
|
+
if (!claimVerification.evaluatorReady || claimVerification.configLoadFailed) {
|
|
274
|
+
warnings.push(claimVerification.recommendation);
|
|
275
|
+
}
|
|
180
276
|
|
|
181
277
|
return {
|
|
182
278
|
generatedAt: new Date().toISOString(),
|
|
@@ -185,10 +281,12 @@ function generateAgentReadinessReport({
|
|
|
185
281
|
runtime,
|
|
186
282
|
bootstrap,
|
|
187
283
|
permissions,
|
|
284
|
+
claimVerification,
|
|
188
285
|
articleAlignment: {
|
|
189
286
|
runtimeIsolation: runtime.isolated,
|
|
190
287
|
contextConditioning: bootstrap.ready,
|
|
191
288
|
permissionEnvelope: permissions.ready,
|
|
289
|
+
factualClaimRecheck: claimVerification.ready,
|
|
192
290
|
},
|
|
193
291
|
warnings,
|
|
194
292
|
};
|
|
@@ -208,6 +306,15 @@ function reportToText(report) {
|
|
|
208
306
|
lines.push(`Permissions: ${report.permissions.profile} (${report.permissions.tier})`);
|
|
209
307
|
lines.push(` Write-capable tools: ${report.permissions.writeCapableTools.length}`);
|
|
210
308
|
lines.push(` Recommendation: ${report.permissions.recommendation}`);
|
|
309
|
+
if (report.claimVerification) {
|
|
310
|
+
lines.push(
|
|
311
|
+
`Claim verification: ${report.claimVerification.ready ? 'ready' : 'needs_attention'}`,
|
|
312
|
+
` Evaluator: ${report.claimVerification.evaluatorReady ? 'present' : 'missing'}`,
|
|
313
|
+
` Verifiers: ${report.claimVerification.verifierCount} (${report.claimVerification.configSource})`,
|
|
314
|
+
` Stop hook: ${report.claimVerification.stopHookRegistered ? 'registered' : 'missing'}`,
|
|
315
|
+
` Recommendation: ${report.claimVerification.recommendation}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
211
318
|
|
|
212
319
|
if (report.warnings.length > 0) {
|
|
213
320
|
lines.push('');
|
|
@@ -226,6 +333,9 @@ module.exports = {
|
|
|
226
333
|
detectRuntimeIsolation,
|
|
227
334
|
collectBootstrapFiles,
|
|
228
335
|
summarizePermissionTier,
|
|
336
|
+
summarizeClaimVerification,
|
|
337
|
+
recommendationForClaimState,
|
|
338
|
+
detectStopHookRegistered,
|
|
229
339
|
generateAgentReadinessReport,
|
|
230
340
|
reportToText,
|
|
231
341
|
};
|
|
@@ -17,6 +17,7 @@ const fs = require('fs');
|
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const {
|
|
19
19
|
cacheUpdateHookCommand,
|
|
20
|
+
claimStopHookCommand,
|
|
20
21
|
codexCacheUpdateHookCommand,
|
|
21
22
|
codexPreToolHookCommand,
|
|
22
23
|
codexSessionStartHookCommand,
|
|
@@ -36,7 +37,7 @@ function getHome() {
|
|
|
36
37
|
// --- Hook definitions ---
|
|
37
38
|
const CLAUDE_HOOKS = {
|
|
38
39
|
PreToolUse: {
|
|
39
|
-
matcher: '
|
|
40
|
+
matcher: '.*',
|
|
40
41
|
hooks: [{ type: 'command', command: preToolHookCommand() }],
|
|
41
42
|
},
|
|
42
43
|
UserPromptSubmit: {
|
|
@@ -49,11 +50,14 @@ const CLAUDE_HOOKS = {
|
|
|
49
50
|
SessionStart: {
|
|
50
51
|
hooks: [{ type: 'command', command: sessionStartHookCommand() }],
|
|
51
52
|
},
|
|
53
|
+
Stop: {
|
|
54
|
+
hooks: [{ type: 'command', command: claimStopHookCommand() }],
|
|
55
|
+
},
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
const CODEX_HOOKS = {
|
|
55
59
|
PreToolUse: {
|
|
56
|
-
matcher: '
|
|
60
|
+
matcher: '.*',
|
|
57
61
|
hooks: [{ type: 'command', command: codexPreToolHookCommand() }],
|
|
58
62
|
},
|
|
59
63
|
UserPromptSubmit: {
|
|
@@ -384,6 +388,7 @@ function wireClaudeHooks(options) {
|
|
|
384
388
|
UserPromptSubmit: /(hook-auto-capture\.sh|hook-auto-capture\b)/,
|
|
385
389
|
PostToolUse: /(hook-thumbgate-cache-updater|cache-update\b)/,
|
|
386
390
|
SessionStart: /(thumbgate_session_start\.sh|session-start\b)/,
|
|
391
|
+
Stop: /(hook-stop-anti-claim\.js|claim-stop-check\b)/,
|
|
387
392
|
};
|
|
388
393
|
|
|
389
394
|
for (const [lifecycle, hookDef] of Object.entries(CLAUDE_HOOKS)) {
|
|
@@ -456,8 +461,13 @@ function codexTomlConfigPath(configPath = codexConfigPath()) {
|
|
|
456
461
|
return path.join(path.dirname(configPath), 'config.toml');
|
|
457
462
|
}
|
|
458
463
|
|
|
464
|
+
function escapeRegexLiteral(value) {
|
|
465
|
+
return String(value).replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
466
|
+
}
|
|
467
|
+
|
|
459
468
|
function tomlSectionRegex(name) {
|
|
460
|
-
|
|
469
|
+
const sectionName = escapeRegexLiteral(name);
|
|
470
|
+
return new RegExp(String.raw`^\[${sectionName}\]\n(?:^(?!\[).*(?:\n|$))*`, 'm');
|
|
461
471
|
}
|
|
462
472
|
|
|
463
473
|
function codexUserPromptTomlBlock() {
|
|
@@ -470,10 +480,11 @@ function upsertCodexUserPromptToml(configPath, dryRun = false) {
|
|
|
470
480
|
const current = fs.existsSync(tomlPath) ? fs.readFileSync(tomlPath, 'utf8') : '';
|
|
471
481
|
const canonicalBlock = codexUserPromptTomlBlock();
|
|
472
482
|
const sectionRegex = tomlSectionRegex('hooks.user_prompt_submit');
|
|
473
|
-
|
|
483
|
+
const existingMatch = sectionRegex.exec(current);
|
|
484
|
+
let next;
|
|
474
485
|
|
|
475
|
-
if (
|
|
476
|
-
const existingBlock =
|
|
486
|
+
if (existingMatch) {
|
|
487
|
+
const existingBlock = existingMatch[0];
|
|
477
488
|
if (existingBlock === canonicalBlock) {
|
|
478
489
|
return { changed: false, settingsPath: tomlPath };
|
|
479
490
|
}
|
|
@@ -610,7 +621,7 @@ function wireGeminiHooks(options) {
|
|
|
610
621
|
if (!hookAlreadyPresent(settings.hooks.PreToolUse, preToolCmd)) {
|
|
611
622
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
|
|
612
623
|
settings.hooks.PreToolUse.push({
|
|
613
|
-
matcher: '
|
|
624
|
+
matcher: '.*',
|
|
614
625
|
hooks: [{ type: 'command', command: preToolCmd }],
|
|
615
626
|
});
|
|
616
627
|
added.push({ lifecycle: 'PreToolUse', command: preToolCmd });
|
|
@@ -661,7 +672,7 @@ function wireForgeHooks(options) {
|
|
|
661
672
|
if (!hookAlreadyPresent(existing.hooks.PreToolUse, preToolCmd)) {
|
|
662
673
|
existing.hooks.PreToolUse = existing.hooks.PreToolUse || [];
|
|
663
674
|
existing.hooks.PreToolUse.push({
|
|
664
|
-
matcher: '
|
|
675
|
+
matcher: '.*',
|
|
665
676
|
hooks: [{ type: 'command', command: preToolCmd }],
|
|
666
677
|
});
|
|
667
678
|
added.push({ lifecycle: 'PreToolUse', command: preToolCmd });
|
|
@@ -765,6 +776,7 @@ module.exports = {
|
|
|
765
776
|
pruneStaleHooksInFile,
|
|
766
777
|
CLAUDE_HOOKS,
|
|
767
778
|
preToolHookCommand,
|
|
779
|
+
claimStopHookCommand,
|
|
768
780
|
userPromptHookCommand,
|
|
769
781
|
sessionStartHookCommand,
|
|
770
782
|
};
|
package/scripts/cli-schema.js
CHANGED
|
@@ -505,6 +505,20 @@ const CLI_COMMANDS = [
|
|
|
505
505
|
group: 'gates',
|
|
506
506
|
flags: [],
|
|
507
507
|
},
|
|
508
|
+
{
|
|
509
|
+
name: 'verify-claims',
|
|
510
|
+
aliases: ['verify-claim'],
|
|
511
|
+
description: 'Recheck factual claims against configured SQLite, filesystem, and JSON sources',
|
|
512
|
+
group: 'gates',
|
|
513
|
+
mcpTool: 'verify_claim',
|
|
514
|
+
flags: [
|
|
515
|
+
{ name: 'claim', type: 'string', required: true, description: 'Factual claim text to verify' },
|
|
516
|
+
{ name: 'config', type: 'string', description: 'Verifier config path (default .thumbgate/claim-verifiers.json)' },
|
|
517
|
+
{ name: 'cwd', type: 'string', description: 'Root directory that contains configured sources' },
|
|
518
|
+
{ name: 'advisory', type: 'boolean', description: 'Do not fail an otherwise parseable claim only because no verifier is configured' },
|
|
519
|
+
{ name: 'json', type: 'boolean', description: 'Output a machine-readable verdict' },
|
|
520
|
+
],
|
|
521
|
+
},
|
|
508
522
|
{
|
|
509
523
|
name: 'hermes-gate',
|
|
510
524
|
description: 'Hermes Agent pre_tool_call hook: gate runtime tool calls (incl. skill_manage) before they run',
|
|
@@ -31,6 +31,9 @@ const {
|
|
|
31
31
|
} = require('./feedback-quality');
|
|
32
32
|
|
|
33
33
|
const INFERRED_TAG_RULES = [
|
|
34
|
+
{ tag: 'claw-style', keywords: ['claw', 'enterprise-claw', 'openshell', 'dynamic-tool', 'screen-interaction', 'computer-use'] },
|
|
35
|
+
{ tag: 'hybrid-inference', keywords: ['hybrid', 'cloud-escalation', 'local-route', 'hybrid-route', 'perplexity-pc'] },
|
|
36
|
+
{ tag: 'agent-identity', keywords: ['agent identity', 'audit trail', 'identity separation', 'agent-credential'] },
|
|
34
37
|
{ tag: 'thumbgate', keywords: ['thumbgate', 'feedback-loop', 'statusline', 'dashboard', 'mcp'] },
|
|
35
38
|
{ tag: 'testing', keywords: ['test', 'testing', 'jest', 'coverage', 'verify', 'verification'] },
|
|
36
39
|
{ tag: 'security', keywords: ['security', 'secret', 'credential', 'token', 'auth'] },
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cross-process lock for append-only local ledgers.
|
|
6
|
+
*
|
|
7
|
+
* `mkdir` is the atomic acquisition primitive. An owner record prevents a
|
|
8
|
+
* crashed process from permanently wedging the control plane, while a nonce
|
|
9
|
+
* check prevents an old owner from deleting a replacement lock.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const crypto = require('node:crypto');
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
|
|
16
|
+
const DEFAULT_STALE_MS = 30 * 1000;
|
|
17
|
+
|
|
18
|
+
function withFileLedgerLock(lockPath, callback, options = {}) {
|
|
19
|
+
const resolvedLockPath = path.resolve(lockPath);
|
|
20
|
+
fs.mkdirSync(path.dirname(resolvedLockPath), { recursive: true });
|
|
21
|
+
const owner = acquireLock(resolvedLockPath, options);
|
|
22
|
+
try {
|
|
23
|
+
if (typeof options.beforeCallback === 'function') options.beforeCallback();
|
|
24
|
+
return callback();
|
|
25
|
+
} finally {
|
|
26
|
+
releaseOwnedLock(resolvedLockPath, owner);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function acquireLock(lockPath, options) {
|
|
31
|
+
const now = options.now || new Date();
|
|
32
|
+
const staleMs = positiveNumber(options.lockStaleMs, DEFAULT_STALE_MS);
|
|
33
|
+
const owner = {
|
|
34
|
+
schemaVersion: 'thumbgate-ledger-lock-v1',
|
|
35
|
+
pid: process.pid,
|
|
36
|
+
nonce: crypto.randomUUID(),
|
|
37
|
+
acquiredAt: now.toISOString(),
|
|
38
|
+
};
|
|
39
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
40
|
+
try {
|
|
41
|
+
fs.mkdirSync(lockPath);
|
|
42
|
+
writeOwner(lockPath, owner);
|
|
43
|
+
return owner;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error.code !== 'EEXIST') throw error;
|
|
46
|
+
if (!recoverStaleLock(lockPath, now, staleMs)) {
|
|
47
|
+
throw lockError(options, 'ledger is busy; deny and retry only after the active writer finishes');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw lockError(options, 'ledger lock could not be acquired after stale-lock recovery');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function recoverStaleLock(lockPath, now, staleMs) {
|
|
55
|
+
const observed = readOwner(lockPath);
|
|
56
|
+
const ageMs = lockAgeMs(lockPath, observed, now);
|
|
57
|
+
if (ageMs < staleMs || processIsAlive(observed?.pid)) return false;
|
|
58
|
+
|
|
59
|
+
// Rename is atomic. If another process already recovered or replaced the
|
|
60
|
+
// lock, this attempt loses harmlessly and acquisition is retried.
|
|
61
|
+
const quarantine = `${lockPath}.stale-${process.pid}-${crypto.randomUUID()}`;
|
|
62
|
+
try {
|
|
63
|
+
fs.renameSync(lockPath, quarantine);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (['ENOENT', 'EEXIST'].includes(error.code)) return true;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
fs.rmSync(quarantine, { recursive: true, force: true });
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeOwner(lockPath, owner) {
|
|
73
|
+
const target = path.join(lockPath, 'owner.json');
|
|
74
|
+
const temporary = `${target}.tmp-${process.pid}-${owner.nonce}`;
|
|
75
|
+
fs.writeFileSync(temporary, `${JSON.stringify(owner)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
76
|
+
fs.renameSync(temporary, target);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readOwner(lockPath) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(fs.readFileSync(path.join(lockPath, 'owner.json'), 'utf8'));
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function lockAgeMs(lockPath, owner, now) {
|
|
88
|
+
const recorded = Date.parse(owner?.acquiredAt || '');
|
|
89
|
+
if (Number.isFinite(recorded)) return Math.max(0, now.getTime() - recorded);
|
|
90
|
+
try {
|
|
91
|
+
return Math.max(0, now.getTime() - fs.statSync(lockPath).mtimeMs);
|
|
92
|
+
} catch {
|
|
93
|
+
return Number.POSITIVE_INFINITY;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function processIsAlive(pid) {
|
|
98
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
99
|
+
try {
|
|
100
|
+
process.kill(pid, 0);
|
|
101
|
+
return true;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return error.code !== 'ESRCH';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function releaseOwnedLock(lockPath, owner) {
|
|
108
|
+
const recorded = readOwner(lockPath);
|
|
109
|
+
if (!recorded || recorded.nonce !== owner.nonce) return;
|
|
110
|
+
try { fs.unlinkSync(path.join(lockPath, 'owner.json')); } catch (error) {
|
|
111
|
+
if (error.code !== 'ENOENT') return;
|
|
112
|
+
}
|
|
113
|
+
try { fs.rmdirSync(lockPath); } catch { /* a replacement owner wins */ }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function positiveNumber(value, fallback) {
|
|
117
|
+
const number = Number(value);
|
|
118
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function lockError(options, message) {
|
|
122
|
+
return typeof options.errorFactory === 'function'
|
|
123
|
+
? options.errorFactory(message)
|
|
124
|
+
: Object.assign(new Error(message), { code: 'THUMBGATE_LEDGER_BUSY' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
DEFAULT_STALE_MS,
|
|
129
|
+
withFileLedgerLock,
|
|
130
|
+
};
|