@ericsanchezok/synergy-util 3.0.11 → 3.0.12

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.
@@ -0,0 +1,13 @@
1
+ export interface FileLockOptions {
2
+ directory: string;
3
+ key: string;
4
+ retryMs?: number;
5
+ timeoutMs?: number;
6
+ staleMetadataMs?: number;
7
+ timeoutMessage?: string;
8
+ }
9
+ export declare const HOLOS_ACCOUNTS_WRITE_LOCK_KEY = "holos-accounts:write";
10
+ export declare const LEGACY_API_KEY_WRITE_LOCK_KEY = "legacy-api-key:write";
11
+ export declare function authLockDirectory(synergyRoot: string): string;
12
+ export declare function fileLockPath(directory: string, key: string): string;
13
+ export declare function withFileLock<T>(options: FileLockOptions, fn: () => Promise<T>): Promise<T>;
@@ -0,0 +1,100 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ const DEFAULT_RETRY_MS = 25;
5
+ const DEFAULT_TIMEOUT_MS = 60_000;
6
+ const DEFAULT_STALE_METADATA_MS = 5_000;
7
+ export const HOLOS_ACCOUNTS_WRITE_LOCK_KEY = "holos-accounts:write";
8
+ export const LEGACY_API_KEY_WRITE_LOCK_KEY = "legacy-api-key:write";
9
+ export function authLockDirectory(synergyRoot) {
10
+ return path.join(synergyRoot, "data", "auth", ".locks");
11
+ }
12
+ export function fileLockPath(directory, key) {
13
+ const digest = createHash("sha256").update(key).digest("hex");
14
+ return path.join(directory, `${digest}.lock`);
15
+ }
16
+ export async function withFileLock(options, fn) {
17
+ const lock = await acquireFileLock(options);
18
+ try {
19
+ return await fn();
20
+ }
21
+ finally {
22
+ await lock.release();
23
+ }
24
+ }
25
+ async function acquireFileLock(options) {
26
+ const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
27
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
28
+ const staleMetadataMs = options.staleMetadataMs ?? DEFAULT_STALE_METADATA_MS;
29
+ await fs.mkdir(options.directory, { recursive: true, mode: 0o700 });
30
+ await fs.chmod(options.directory, 0o700);
31
+ const filename = fileLockPath(options.directory, options.key);
32
+ const startedAt = Date.now();
33
+ while (true) {
34
+ try {
35
+ const handle = await fs.open(filename, "wx", 0o600);
36
+ try {
37
+ await handle.writeFile(JSON.stringify({ pid: process.pid, acquiredAt: Date.now() }));
38
+ }
39
+ catch (error) {
40
+ await fs.unlink(filename).catch(() => { });
41
+ throw error;
42
+ }
43
+ finally {
44
+ await handle.close();
45
+ }
46
+ return {
47
+ async release() {
48
+ await fs.unlink(filename).catch((error) => {
49
+ if (error.code !== "ENOENT")
50
+ throw error;
51
+ });
52
+ },
53
+ };
54
+ }
55
+ catch (error) {
56
+ if (error.code !== "EEXIST")
57
+ throw error;
58
+ const owner = await readLockOwner(filename);
59
+ if (owner?.pid && !processExists(owner.pid)) {
60
+ await fs.unlink(filename).catch(() => { });
61
+ continue;
62
+ }
63
+ if (!owner?.pid) {
64
+ const stat = await fs.stat(filename).catch(() => undefined);
65
+ if (stat && Date.now() - stat.mtimeMs > staleMetadataMs) {
66
+ await fs.unlink(filename).catch(() => { });
67
+ continue;
68
+ }
69
+ }
70
+ if (Date.now() - startedAt >= timeoutMs) {
71
+ throw new Error(options.timeoutMessage ?? `Timed out acquiring file lock for ${options.key}`);
72
+ }
73
+ await sleep(retryMs);
74
+ }
75
+ }
76
+ }
77
+ async function readLockOwner(filename) {
78
+ try {
79
+ const owner = JSON.parse(await fs.readFile(filename, "utf8"));
80
+ if (!owner || typeof owner !== "object")
81
+ return undefined;
82
+ const pid = owner.pid;
83
+ return typeof pid === "number" ? { pid } : undefined;
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ }
89
+ function processExists(pid) {
90
+ try {
91
+ process.kill(pid, 0);
92
+ return true;
93
+ }
94
+ catch (error) {
95
+ return error.code !== "ESRCH";
96
+ }
97
+ }
98
+ async function sleep(milliseconds) {
99
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
100
+ }
@@ -10,6 +10,11 @@ export interface RuntimeLimits {
10
10
  heartbeatIntervalMs: number;
11
11
  maxMemoryMb: number;
12
12
  memorySampleIntervalMs: number;
13
+ agentCallMaxRuntimeMs: number;
14
+ hookTimeoutMs: number;
15
+ contributionInvokeTimeoutMs: number;
16
+ shellRunTimeoutMs: number;
17
+ taskRunWaitTimeoutMs: number;
13
18
  }
14
19
  export type RuntimeLimitOverrides = Partial<RuntimeLimits>;
15
20
  export interface PluginRuntimePolicyInput {
@@ -8,6 +8,11 @@ export const DEFAULT_PLUGIN_RUNTIME_LIMITS = {
8
8
  heartbeatIntervalMs: 5_000,
9
9
  maxMemoryMb: 512,
10
10
  memorySampleIntervalMs: 5_000,
11
+ agentCallMaxRuntimeMs: 120_000,
12
+ hookTimeoutMs: 120_000,
13
+ contributionInvokeTimeoutMs: 120_000,
14
+ shellRunTimeoutMs: 120_000,
15
+ taskRunWaitTimeoutMs: 120_000,
11
16
  };
12
17
  export function resolveRuntimeMode(input) {
13
18
  return input.source === "builtin" ? "inProcess" : "process";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericsanchezok/synergy-util",
3
- "version": "3.0.11",
3
+ "version": "3.0.12",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "exports": {