@phnx-labs/agents-cli 1.20.20 → 1.20.21

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.
@@ -265,6 +265,9 @@ export async function runDaemon() {
265
265
  scheduler.reloadAll();
266
266
  const reloaded = scheduler.listScheduled();
267
267
  log('INFO', `Reloaded ${reloaded.length} jobs`);
268
+ // Drop the memoized R2 config so rotated/added sync credentials are re-read
269
+ // on the next cycle instead of waiting for a restart.
270
+ void import('./session/sync/config.js').then(m => m.clearR2ConfigCache());
268
271
  };
269
272
  const handleShutdown = async () => {
270
273
  log('INFO', 'Daemon shutting down');
@@ -13,14 +13,26 @@ export interface R2Config {
13
13
  /** S3-compatible endpoint for the account (no bucket, no trailing slash). */
14
14
  endpoint: string;
15
15
  }
16
+ /** Window after a prompt-bearing resolution failure during which we skip
17
+ * re-attempting (and thus re-prompting). SIGHUP / restart bypasses it. */
18
+ export declare const RESOLVE_RETRY_COOLDOWN_MS: number;
19
+ /** Drop the cached resolution so the next call reads the bundle fresh. Called on
20
+ * daemon SIGHUP (to pick up rotated credentials) and between tests. */
21
+ export declare function clearR2ConfigCache(): void;
16
22
  /**
17
- * Resolve R2 credentials from the `r2.backups` bundle. Throws a clear,
18
- * actionable error if the bundle or any key is missing sync cannot proceed
19
- * without real credentials (no silent fallback).
23
+ * Resolve R2 credentials, reading the keychain at most once per process. The
24
+ * first call reads (and may prompt for Touch ID); every later call returns the
25
+ * memoized result. Throws if the bundle/keys are missing — failures are not
26
+ * memoized, but see isSyncConfigured for the re-prompt cooldown.
20
27
  */
21
28
  export declare function loadR2Config(): R2Config;
22
- /** True when the sync bundle exists and looks resolvable, without throwing. */
23
- export declare function isSyncConfigured(): boolean;
29
+ /**
30
+ * True when the sync bundle exists and resolves, without throwing. After a
31
+ * prompt-bearing failure (e.g. a cancelled Touch ID) it returns false without
32
+ * re-reading the keychain for RESOLVE_RETRY_COOLDOWN_MS, so a dismissed prompt
33
+ * does not re-storm every cycle. `now` is injectable for tests.
34
+ */
35
+ export declare function isSyncConfigured(now?: number): boolean;
24
36
  /**
25
37
  * This machine's stable, human-readable id, used as its R2 prefix and mirror
26
38
  * directory name. Tailnet hostnames (zion, yosemite-s0, mac-mini) are already
@@ -12,7 +12,7 @@ export const SYNC_BUNDLE = 'r2.backups';
12
12
  * actionable error if the bundle or any key is missing — sync cannot proceed
13
13
  * without real credentials (no silent fallback).
14
14
  */
15
- export function loadR2Config() {
15
+ function resolveR2Config() {
16
16
  const { env } = readAndResolveBundleEnv(SYNC_BUNDLE, { caller: 'sessions-sync' });
17
17
  const accountId = env.R2_ACCOUNT_ID?.trim();
18
18
  const bucket = env.R2_BUCKET_NAME?.trim();
@@ -36,13 +36,60 @@ export function loadR2Config() {
36
36
  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
37
37
  };
38
38
  }
39
- /** True when the sync bundle exists and looks resolvable, without throwing. */
40
- export function isSyncConfigured() {
39
+ // ── Resolution cache ────────────────────────────────────────────────────────
40
+ // The daemon calls isSyncConfigured() + syncSessions() every ~90s, and each used
41
+ // to trigger a fresh read of the biometry-gated `r2.backups` keychain items —
42
+ // one Touch ID prompt per gated item, every cycle, forever. We instead resolve
43
+ // at most once per process: a success is memoized for the process lifetime
44
+ // (cleared on daemon SIGHUP via clearR2ConfigCache), so subsequent cycles never
45
+ // touch the keychain again. A *prompt-bearing* failure (cancelled Touch ID, etc.)
46
+ // starts a cooldown so a dismissed prompt is not re-issued every cycle. A simply
47
+ // absent bundle never prompts, so it is re-checked each cycle (fast pickup when
48
+ // the user later adds credentials).
49
+ let cachedConfig = null;
50
+ let lastPromptFailureAt = 0;
51
+ /** Window after a prompt-bearing resolution failure during which we skip
52
+ * re-attempting (and thus re-prompting). SIGHUP / restart bypasses it. */
53
+ export const RESOLVE_RETRY_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes
54
+ /** Drop the cached resolution so the next call reads the bundle fresh. Called on
55
+ * daemon SIGHUP (to pick up rotated credentials) and between tests. */
56
+ export function clearR2ConfigCache() {
57
+ cachedConfig = null;
58
+ lastPromptFailureAt = 0;
59
+ }
60
+ /**
61
+ * Resolve R2 credentials, reading the keychain at most once per process. The
62
+ * first call reads (and may prompt for Touch ID); every later call returns the
63
+ * memoized result. Throws if the bundle/keys are missing — failures are not
64
+ * memoized, but see isSyncConfigured for the re-prompt cooldown.
65
+ */
66
+ export function loadR2Config() {
67
+ if (cachedConfig)
68
+ return cachedConfig;
69
+ cachedConfig = resolveR2Config();
70
+ return cachedConfig;
71
+ }
72
+ /**
73
+ * True when the sync bundle exists and resolves, without throwing. After a
74
+ * prompt-bearing failure (e.g. a cancelled Touch ID) it returns false without
75
+ * re-reading the keychain for RESOLVE_RETRY_COOLDOWN_MS, so a dismissed prompt
76
+ * does not re-storm every cycle. `now` is injectable for tests.
77
+ */
78
+ export function isSyncConfigured(now = Date.now()) {
79
+ if (cachedConfig)
80
+ return true;
81
+ if (lastPromptFailureAt && now - lastPromptFailureAt < RESOLVE_RETRY_COOLDOWN_MS)
82
+ return false;
41
83
  try {
42
84
  loadR2Config();
43
85
  return true;
44
86
  }
45
- catch {
87
+ catch (err) {
88
+ // A missing bundle never prompts, so keep re-checking it each cycle (so a
89
+ // later `agents secrets add` is picked up quickly). Any other failure may
90
+ // have cost a prompt (cancelled Touch ID, keychain error) — back off.
91
+ if (!/not found/i.test(err.message))
92
+ lastPromptFailureAt = now;
46
93
  return false;
47
94
  }
48
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.20",
3
+ "version": "1.20.21",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",