@saadnvd1/agent-os 0.3.733 → 0.3.734

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.
@@ -313,14 +313,20 @@ export async function orchestratorWriteBounds(
313
313
  // Read at most once a minute, not on every write.
314
314
  const HOOKS_TTL_MS = 60_000;
315
315
  let hooksCache: { key: string; at: number; dirs: string[] } | null = null;
316
+ // The last answer git gave, kept apart from the cache above: a read that
317
+ // times out under load may fall back to one this recent, since a hooks folder
318
+ // rarely moves in minutes. It is never refreshed from a fallback.
319
+ const HOOKS_STALE_OK_MS = 10 * 60_000;
320
+ const lastGoodHooks = new Map<string, { at: number; dirs: string[] }>();
316
321
 
317
322
  export async function hooksPaths(repos: string[]): Promise<string[]> {
318
323
  const key = repos.join("\0");
319
324
  if (hooksCache?.key === key && Date.now() - hooksCache.at < HOOKS_TTL_MS)
320
325
  return hooksCache.dirs;
321
326
  // Only "not set" and "not a repository" mean there's nothing to protect;
322
- // any other failure (a timeout under load) throws, so the write is
323
- // refused and nothing is cached.
327
+ // any other failure (a timeout under load) uses the last good answer if
328
+ // it's under ten minutes old, else throws, so the write is refused and
329
+ // nothing is cached.
324
330
  const run = (cwd: string, args: string[]) =>
325
331
  git(cwd, args, { timeout: 5000 })
326
332
  .then((out) => out.trim())
@@ -336,33 +342,47 @@ export async function hooksPaths(repos: string[]): Promise<string[]> {
336
342
  // The user's, expanded by git (~ and ~user). A relative one is taken from
337
343
  // each repo's top, so it names no one folder: each project's answer
338
344
  // below covers it there. Each project's comes as git itself resolves it.
339
- const global = await run(os.homedir(), [
340
- "config",
341
- "--global",
342
- "--type=path",
343
- "--get",
344
- "core.hooksPath",
345
- ]);
346
- const found = await Promise.all(
347
- repos.map(async (r) => {
348
- // A project whose folder is gone has no hooks to run.
349
- if (!fs.existsSync(r)) return "";
350
- const dir = await run(r, [
351
- "rev-parse",
352
- "--path-format=absolute",
353
- "--git-path",
354
- "hooks",
355
- ]);
356
- if (dir !== "" && !dir.startsWith("/"))
357
- throw new Error(`git named no folder for ${r}'s hooks`);
358
- return dir;
359
- })
360
- );
361
- const dirs = [
362
- ...new Set([global, ...found].filter((d) => d.startsWith("/"))),
363
- ];
364
- hooksCache = { key, at: Date.now(), dirs };
365
- return dirs;
345
+ try {
346
+ return await read();
347
+ } catch (e) {
348
+ const good = lastGoodHooks.get(key);
349
+ if (!good || Date.now() - good.at >= HOOKS_STALE_OK_MS) throw e;
350
+ console.warn(
351
+ `[write-guard] ${e instanceof Error ? e.message : e}; using the answer from ${Math.round((Date.now() - good.at) / 1000)}s ago`
352
+ );
353
+ return good.dirs;
354
+ }
355
+
356
+ async function read(): Promise<string[]> {
357
+ const global = await run(os.homedir(), [
358
+ "config",
359
+ "--global",
360
+ "--type=path",
361
+ "--get",
362
+ "core.hooksPath",
363
+ ]);
364
+ const found = await Promise.all(
365
+ repos.map(async (r) => {
366
+ // A project whose folder is gone has no hooks to run.
367
+ if (!fs.existsSync(r)) return "";
368
+ const dir = await run(r, [
369
+ "rev-parse",
370
+ "--path-format=absolute",
371
+ "--git-path",
372
+ "hooks",
373
+ ]);
374
+ if (dir !== "" && !dir.startsWith("/"))
375
+ throw new Error(`git named no folder for ${r}'s hooks`);
376
+ return dir;
377
+ })
378
+ );
379
+ const dirs = [
380
+ ...new Set([global, ...found].filter((d) => d.startsWith("/"))),
381
+ ];
382
+ hooksCache = { key, at: Date.now(), dirs };
383
+ lastGoodHooks.set(key, { at: Date.now(), dirs });
384
+ return dirs;
385
+ }
366
386
  }
367
387
 
368
388
  // The check a PreToolUse hook makes: why this tool call is refused, or null.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.733",
3
+ "version": "0.3.734",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"