@phnx-labs/agents-cli 1.22.28 → 1.22.30
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/CHANGELOG.md +88 -0
- package/README.md +39 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.d.ts +13 -0
- package/dist/commands/accounts.js +32 -0
- package/dist/commands/daemon.d.ts +18 -0
- package/dist/commands/daemon.js +581 -0
- package/dist/commands/exec.js +66 -20
- package/dist/commands/routines.js +29 -11
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +30 -15
- package/dist/commands/sessions-browser.js +6 -6
- package/dist/commands/sessions-favorite.d.ts +7 -7
- package/dist/commands/sessions-favorite.js +30 -30
- package/dist/commands/sessions-picker.d.ts +33 -1
- package/dist/commands/sessions-picker.js +102 -27
- package/dist/commands/sessions.d.ts +12 -1
- package/dist/commands/sessions.js +259 -20
- package/dist/commands/view.d.ts +11 -0
- package/dist/commands/view.js +56 -29
- package/dist/index.js +37 -2
- package/dist/lib/account-labels.d.ts +24 -0
- package/dist/lib/account-labels.js +72 -0
- package/dist/lib/agents.d.ts +32 -1
- package/dist/lib/agents.js +96 -31
- package/dist/lib/daemon-health.d.ts +24 -0
- package/dist/lib/daemon-health.js +84 -0
- package/dist/lib/daemon-ticks.d.ts +81 -0
- package/dist/lib/daemon-ticks.js +190 -0
- package/dist/lib/daemon.d.ts +68 -18
- package/dist/lib/daemon.js +303 -338
- package/dist/lib/device-config.d.ts +10 -0
- package/dist/lib/device-config.js +27 -0
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +49 -2
- package/dist/lib/hosts/dispatch.d.ts +4 -0
- package/dist/lib/hosts/dispatch.js +4 -0
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +1 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/import.js +7 -6
- package/dist/lib/memory-cache.d.ts +19 -0
- package/dist/lib/memory-cache.js +31 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/snapshot.d.ts +16 -0
- package/dist/lib/menubar/snapshot.js +22 -1
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +7 -2
- package/dist/lib/routine-activation.d.ts +2 -0
- package/dist/lib/routine-activation.js +16 -0
- package/dist/lib/runner.d.ts +18 -0
- package/dist/lib/runner.js +52 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +19 -0
- package/dist/lib/secrets/agent.js +32 -2
- package/dist/lib/secrets/scope.d.ts +3 -3
- package/dist/lib/secrets/scope.js +3 -3
- package/dist/lib/session/db.d.ts +15 -0
- package/dist/lib/session/db.js +90 -15
- package/dist/lib/session/discover.js +91 -39
- package/dist/lib/session/favorites.d.ts +2 -2
- package/dist/lib/session/favorites.js +2 -2
- package/dist/lib/session/parse.d.ts +63 -0
- package/dist/lib/session/parse.js +165 -20
- package/dist/lib/session/session-cache.d.ts +9 -6
- package/dist/lib/session/session-cache.js +23 -6
- package/dist/lib/shims.js +12 -0
- package/dist/lib/startup/command-registry.d.ts +15 -1
- package/dist/lib/startup/command-registry.js +49 -0
- package/dist/lib/usage-refresh.js +3 -2
- package/dist/lib/usage.d.ts +12 -10
- package/dist/lib/usage.js +81 -154
- package/package.json +4 -1
|
@@ -73,6 +73,16 @@ export declare function isSchedulerEnabled(): boolean;
|
|
|
73
73
|
* scheduler init) refuses with.
|
|
74
74
|
*/
|
|
75
75
|
export declare function assertSchedulerEnabled(): void;
|
|
76
|
+
/** True unless this machine's device doc disables the daemon outright (top-level kill switch). */
|
|
77
|
+
export declare function isDaemonEnabled(): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Throw when the daemon is disabled on this machine, naming the setting and
|
|
80
|
+
* the fix. Every AUTO-start surface (routines add/start/catchup/webhook,
|
|
81
|
+
* `ensureDaemonStarted`) refuses with this before calling `startDaemon()`.
|
|
82
|
+
* `agents daemon start` is the deliberate override and does NOT call this —
|
|
83
|
+
* disable only blocks auto-start, mirroring `systemctl disable`.
|
|
84
|
+
*/
|
|
85
|
+
export declare function assertDaemonEnabled(): void;
|
|
76
86
|
/**
|
|
77
87
|
* Read the `agents.max-concurrent` cap for each named device from its synced
|
|
78
88
|
* device doc (no SSH). Devices without a cap are omitted — uncapped is the
|
|
@@ -67,6 +67,16 @@ export const CONFIG_KEYS = [
|
|
|
67
67
|
type: 'bool',
|
|
68
68
|
description: 'Whether the routines scheduler (daemon) may fire on this device.',
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
name: 'daemon.enabled',
|
|
72
|
+
yamlKey: 'daemonEnabled',
|
|
73
|
+
scope: 'device',
|
|
74
|
+
type: 'bool',
|
|
75
|
+
description: 'Whether the daemon may run on this device at all (secrets broker, browser IPC, watchdog, and the ' +
|
|
76
|
+
'routines scheduler). Disabling is the top-level kill switch: nothing auto-starts the daemon while it ' +
|
|
77
|
+
'is set, including `routines add`/`routines start`/`routines catchup`/webhook triggers. ' +
|
|
78
|
+
'`agents daemon start` still starts it explicitly.',
|
|
79
|
+
},
|
|
70
80
|
{
|
|
71
81
|
name: 'watchdog.enabled',
|
|
72
82
|
yamlKey: 'watchdogEnabled',
|
|
@@ -294,6 +304,23 @@ export function assertSchedulerEnabled() {
|
|
|
294
304
|
throw new Error(`The routines scheduler is disabled on this device (scheduler.enabled=false in ~/.agents/devices/${machineId()}/agents.yaml). ` +
|
|
295
305
|
`Re-enable with: agents devices configure ${machineId()} --scheduler on`);
|
|
296
306
|
}
|
|
307
|
+
/** True unless this machine's device doc disables the daemon outright (top-level kill switch). */
|
|
308
|
+
export function isDaemonEnabled() {
|
|
309
|
+
return getConfigValue('daemon.enabled').value !== false;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Throw when the daemon is disabled on this machine, naming the setting and
|
|
313
|
+
* the fix. Every AUTO-start surface (routines add/start/catchup/webhook,
|
|
314
|
+
* `ensureDaemonStarted`) refuses with this before calling `startDaemon()`.
|
|
315
|
+
* `agents daemon start` is the deliberate override and does NOT call this —
|
|
316
|
+
* disable only blocks auto-start, mirroring `systemctl disable`.
|
|
317
|
+
*/
|
|
318
|
+
export function assertDaemonEnabled() {
|
|
319
|
+
if (isDaemonEnabled())
|
|
320
|
+
return;
|
|
321
|
+
throw new Error(`The daemon is disabled on this device (daemon.enabled=false in ~/.agents/devices/${machineId()}/agents.yaml). ` +
|
|
322
|
+
`Re-enable with: agents daemon enable`);
|
|
323
|
+
}
|
|
297
324
|
/**
|
|
298
325
|
* Read the `agents.max-concurrent` cap for each named device from its synced
|
|
299
326
|
* device doc (no SSH). Devices without a cap are omitted — uncapped is the
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -288,6 +288,33 @@ export declare function nativeResume(agent: AgentId, version?: string): boolean;
|
|
|
288
288
|
* only accept `-c` config overrides.
|
|
289
289
|
*/
|
|
290
290
|
export declare function codexWritableRootsConfig(dir: string): string;
|
|
291
|
+
/**
|
|
292
|
+
* Resolve the executable `buildExecCommand` will put in `cmd[0]`, or null when
|
|
293
|
+
* that resolution finds nothing on disk.
|
|
294
|
+
*
|
|
295
|
+
* This is an EXISTENCE probe, not an "is it managed by us" check. A harness the
|
|
296
|
+
* user installed themselves (Homebrew, a vendor `curl | sh`, a distro package)
|
|
297
|
+
* has no version home at all, and running it is a supported state — so with no
|
|
298
|
+
* version pinned we answer with a PATH lookup of the bare launch command, the
|
|
299
|
+
* same thing `spawnAgent` would resolve.
|
|
300
|
+
*
|
|
301
|
+
* `findInPath` deliberately excludes our own shims dir, because a shim is a
|
|
302
|
+
* dispatcher rather than an install. That exclusion alone is too strong here:
|
|
303
|
+
* the shim DOES launch whenever agents-cli owns at least one version of the
|
|
304
|
+
* agent — it resolves the version itself, and when no default is pinned it
|
|
305
|
+
* prints its own accurate `no default set … agents use <agent> <version>`
|
|
306
|
+
* guidance. Pre-empting that with "not installed" would name the wrong fix
|
|
307
|
+
* (`agents add`) for a machine that already has the harness. So the shim counts
|
|
308
|
+
* only when a managed version exists; with zero managed versions it is the dead
|
|
309
|
+
* end this probe was written to catch (RUSH-2339).
|
|
310
|
+
*
|
|
311
|
+
* The version-pinned branch mirrors buildExecCommand exactly: versioned shim
|
|
312
|
+
* first, then the version home's real binary. It deliberately does NOT fall back
|
|
313
|
+
* to PATH — with a version pinned, buildExecCommand spawns the literal
|
|
314
|
+
* `<cli>@<version>`, which is not on PATH, so a PATH hit here would be a lie
|
|
315
|
+
* that still exits 127.
|
|
316
|
+
*/
|
|
317
|
+
export declare function resolveLaunchBinary(agent: AgentId, version?: string): string | null;
|
|
291
318
|
/** Assemble the full CLI argument array for an agent invocation. */
|
|
292
319
|
export declare function buildExecCommand(options: ExecOptions): string[];
|
|
293
320
|
/** Spawn an agent and return its exit code. Convenience wrapper over spawnAgent. */
|
package/dist/lib/exec.js
CHANGED
|
@@ -9,9 +9,9 @@ import { randomUUID } from 'crypto';
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { ALL_MODES } from './types.js';
|
|
12
|
-
import { AGENTS } from './agents.js';
|
|
12
|
+
import { AGENTS, findInPath } from './agents.js';
|
|
13
13
|
import { parseTimeout } from './routines.js';
|
|
14
|
-
import { compareVersions, getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
|
|
14
|
+
import { compareVersions, getBinaryPath, getVersionHomePath, isVersionInstalled, listInstalledVersions, resolveVersion } from './versions.js';
|
|
15
15
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
16
16
|
import { isTierToken, resolveTier } from './model-tiers.js';
|
|
17
17
|
import { createTimer, redactPrompt, redactArgs } from './events.js';
|
|
@@ -725,6 +725,53 @@ export function nativeResume(agent, version) {
|
|
|
725
725
|
export function codexWritableRootsConfig(dir) {
|
|
726
726
|
return `sandbox_workspace_write.writable_roots=[${JSON.stringify(dir)}]`;
|
|
727
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* Resolve the executable `buildExecCommand` will put in `cmd[0]`, or null when
|
|
730
|
+
* that resolution finds nothing on disk.
|
|
731
|
+
*
|
|
732
|
+
* This is an EXISTENCE probe, not an "is it managed by us" check. A harness the
|
|
733
|
+
* user installed themselves (Homebrew, a vendor `curl | sh`, a distro package)
|
|
734
|
+
* has no version home at all, and running it is a supported state — so with no
|
|
735
|
+
* version pinned we answer with a PATH lookup of the bare launch command, the
|
|
736
|
+
* same thing `spawnAgent` would resolve.
|
|
737
|
+
*
|
|
738
|
+
* `findInPath` deliberately excludes our own shims dir, because a shim is a
|
|
739
|
+
* dispatcher rather than an install. That exclusion alone is too strong here:
|
|
740
|
+
* the shim DOES launch whenever agents-cli owns at least one version of the
|
|
741
|
+
* agent — it resolves the version itself, and when no default is pinned it
|
|
742
|
+
* prints its own accurate `no default set … agents use <agent> <version>`
|
|
743
|
+
* guidance. Pre-empting that with "not installed" would name the wrong fix
|
|
744
|
+
* (`agents add`) for a machine that already has the harness. So the shim counts
|
|
745
|
+
* only when a managed version exists; with zero managed versions it is the dead
|
|
746
|
+
* end this probe was written to catch (RUSH-2339).
|
|
747
|
+
*
|
|
748
|
+
* The version-pinned branch mirrors buildExecCommand exactly: versioned shim
|
|
749
|
+
* first, then the version home's real binary. It deliberately does NOT fall back
|
|
750
|
+
* to PATH — with a version pinned, buildExecCommand spawns the literal
|
|
751
|
+
* `<cli>@<version>`, which is not on PATH, so a PATH hit here would be a lie
|
|
752
|
+
* that still exits 127.
|
|
753
|
+
*/
|
|
754
|
+
export function resolveLaunchBinary(agent, version) {
|
|
755
|
+
const command = AGENT_COMMANDS[agent].base[0];
|
|
756
|
+
if (version) {
|
|
757
|
+
const versionedShim = path.join(getShimsDir(), `${command}@${version}`);
|
|
758
|
+
if (process.platform === 'win32' && fs.existsSync(versionedShim + '.cmd')) {
|
|
759
|
+
return versionedShim + '.cmd';
|
|
760
|
+
}
|
|
761
|
+
if (fs.existsSync(versionedShim))
|
|
762
|
+
return versionedShim;
|
|
763
|
+
const binary = getBinaryPath(agent, version);
|
|
764
|
+
return binary && fs.existsSync(binary) ? binary : null;
|
|
765
|
+
}
|
|
766
|
+
const native = findInPath(command);
|
|
767
|
+
if (native)
|
|
768
|
+
return native;
|
|
769
|
+
if (listInstalledVersions(agent).length === 0)
|
|
770
|
+
return null;
|
|
771
|
+
// Re-scan PATH accepting the shim: point findInPath's exclusion at a path that
|
|
772
|
+
// matches nothing, so the real shims dir participates like any other PATH entry.
|
|
773
|
+
return findInPath(command, { shimsDir: path.join(getShimsDir(), '.no-such-dir') });
|
|
774
|
+
}
|
|
728
775
|
/** Assemble the full CLI argument array for an agent invocation. */
|
|
729
776
|
export function buildExecCommand(options) {
|
|
730
777
|
const template = AGENT_COMMANDS[options.agent];
|
|
@@ -118,6 +118,8 @@ export interface DispatchOptions {
|
|
|
118
118
|
version?: string;
|
|
119
119
|
/** Run strategy (e.g. "balanced") — the remote picks among ITS signed-in accounts. */
|
|
120
120
|
strategy?: string;
|
|
121
|
+
/** Named provider account — the remote resolves it against ITS signed-in versions. */
|
|
122
|
+
account?: string;
|
|
121
123
|
balanced?: boolean;
|
|
122
124
|
fallback?: string;
|
|
123
125
|
mode?: string;
|
|
@@ -193,6 +195,8 @@ export interface InteractiveDispatchOptions {
|
|
|
193
195
|
version?: string;
|
|
194
196
|
/** Explicit run strategy (e.g. "balanced") to forward as `--strategy <strategy>`. */
|
|
195
197
|
strategy?: string;
|
|
198
|
+
/** Named provider account to resolve on the remote host. */
|
|
199
|
+
account?: string;
|
|
196
200
|
/** Optional prompt — forwarded only when the caller explicitly forced interactive mode. */
|
|
197
201
|
prompt?: string;
|
|
198
202
|
mode?: string;
|
|
@@ -401,6 +401,8 @@ export function buildRunForwardedArgs(opts) {
|
|
|
401
401
|
args.push('--timeout', opts.timeout);
|
|
402
402
|
if (opts.strategy)
|
|
403
403
|
args.push('--strategy', opts.strategy);
|
|
404
|
+
if (opts.account)
|
|
405
|
+
args.push('--account', opts.account);
|
|
404
406
|
if (opts.balanced)
|
|
405
407
|
args.push('--balanced');
|
|
406
408
|
if (opts.fallback)
|
|
@@ -466,6 +468,8 @@ export function buildInteractiveRunForwardedArgs(opts) {
|
|
|
466
468
|
args.push('--timeout', opts.timeout);
|
|
467
469
|
if (opts.strategy)
|
|
468
470
|
args.push('--strategy', opts.strategy);
|
|
471
|
+
if (opts.account)
|
|
472
|
+
args.push('--account', opts.account);
|
|
469
473
|
if (opts.balanced)
|
|
470
474
|
args.push('--balanced');
|
|
471
475
|
if (opts.fallback)
|
package/dist/lib/import.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import * as fs from 'fs';
|
|
20
20
|
import * as os from 'os';
|
|
21
21
|
import * as path from 'path';
|
|
22
|
-
import { AGENTS } from './agents.js';
|
|
22
|
+
import { AGENTS, resolveNativeBinaryPath } from './agents.js';
|
|
23
23
|
import { getUserAgentsDir, getVersionsDir } from './state.js';
|
|
24
24
|
import { setGlobalDefault } from './versions.js';
|
|
25
25
|
import { createShim, createVersionedAlias, ensureShimCurrent, switchHomeFileSymlinks, assertIsolationBoundary } from './shims.js';
|
|
@@ -200,15 +200,16 @@ export function importInstallScriptBinary(spec, version, binaryPath, versionDir)
|
|
|
200
200
|
if (alreadyExists) {
|
|
201
201
|
return { success: false, skipped: true, error: `${version} already installed`, resolvedFromPath: binaryPath };
|
|
202
202
|
}
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
const nativeBinary = resolveNativeBinaryPath(spec.cliCommand, binaryPath);
|
|
204
|
+
if (!nativeBinary) {
|
|
205
|
+
return { success: false, error: `Binary does not resolve to a native executable: ${binaryPath}` };
|
|
205
206
|
}
|
|
206
207
|
try {
|
|
207
208
|
fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
|
|
208
209
|
fs.mkdirSync(path.join(versionDir, 'node_modules', '.bin'), { recursive: true });
|
|
209
|
-
fs.writeFileSync(path.join(versionDir, 'package.json'), JSON.stringify({ name: `agents-${spec.agentId}-${version}`, version: '1.0.0', private: true, imported: true, from:
|
|
210
|
-
fs.symlinkSync(
|
|
211
|
-
return { success: true, resolvedFromPath:
|
|
210
|
+
fs.writeFileSync(path.join(versionDir, 'package.json'), JSON.stringify({ name: `agents-${spec.agentId}-${version}`, version: '1.0.0', private: true, imported: true, from: nativeBinary, installScriptBased: true }, null, 2));
|
|
211
|
+
fs.symlinkSync(nativeBinary, binaryLink);
|
|
212
|
+
return { success: true, resolvedFromPath: nativeBinary };
|
|
212
213
|
}
|
|
213
214
|
catch (err) {
|
|
214
215
|
return { success: false, error: err.message };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { LRUCache } from 'lru-cache';
|
|
2
|
+
export interface MemoryCacheOptions<K extends {} = string, V extends {} = {}> {
|
|
3
|
+
/** Hard entry bound. A cache without a bound is not allowed. */
|
|
4
|
+
max: number;
|
|
5
|
+
/** Milliseconds before an entry is treated as absent. */
|
|
6
|
+
ttlMs: number;
|
|
7
|
+
/** Optional async loader. Concurrent fetches for one key are coalesced. */
|
|
8
|
+
fetchMethod?: (key: K, staleValue: V | undefined) => Promise<V>;
|
|
9
|
+
/** Injectable monotonic clock for deterministic TTL tests. */
|
|
10
|
+
now?: () => number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Correctness-first process-local cache defaults.
|
|
14
|
+
*
|
|
15
|
+
* The cache is always bounded, never returns stale values, and does not extend
|
|
16
|
+
* an entry's life merely because it was read. Durable/cross-process state must
|
|
17
|
+
* continue to live in SQLite or an atomic on-disk snapshot.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createMemoryCache<K extends {}, V extends {}>(options: MemoryCacheOptions<K, V>): LRUCache<K, V>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { LRUCache } from 'lru-cache';
|
|
2
|
+
/**
|
|
3
|
+
* Correctness-first process-local cache defaults.
|
|
4
|
+
*
|
|
5
|
+
* The cache is always bounded, never returns stale values, and does not extend
|
|
6
|
+
* an entry's life merely because it was read. Durable/cross-process state must
|
|
7
|
+
* continue to live in SQLite or an atomic on-disk snapshot.
|
|
8
|
+
*/
|
|
9
|
+
export function createMemoryCache(options) {
|
|
10
|
+
if (!Number.isSafeInteger(options.max) || options.max < 1) {
|
|
11
|
+
throw new Error('memory cache max must be a positive integer');
|
|
12
|
+
}
|
|
13
|
+
if (!Number.isFinite(options.ttlMs) || options.ttlMs <= 0) {
|
|
14
|
+
throw new Error('memory cache ttlMs must be positive');
|
|
15
|
+
}
|
|
16
|
+
return new LRUCache({
|
|
17
|
+
max: options.max,
|
|
18
|
+
ttl: options.ttlMs,
|
|
19
|
+
allowStale: false,
|
|
20
|
+
updateAgeOnGet: false,
|
|
21
|
+
updateAgeOnHas: false,
|
|
22
|
+
ttlAutopurge: false,
|
|
23
|
+
// A supplied deterministic clock must be read on every operation; the
|
|
24
|
+
// library's 1ms cached-now optimization follows wall time, not that clock.
|
|
25
|
+
ttlResolution: options.now ? 0 : 1,
|
|
26
|
+
fetchMethod: options.fetchMethod
|
|
27
|
+
? async (key, staleValue) => options.fetchMethod(key, staleValue)
|
|
28
|
+
: undefined,
|
|
29
|
+
perf: options.now ? { now: options.now } : undefined,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import type { WatchdogTickResult } from '../watchdog/runner.js';
|
|
2
|
+
/**
|
|
3
|
+
* One registered fleet device, for the menu-bar's collapsible DEVICES section.
|
|
4
|
+
* Sourced from the local registry read (`loadDevices`) — no network probe — so
|
|
5
|
+
* it carries only what the registry knows for sure (name, platform, whether it
|
|
6
|
+
* is the interactive host, whether it is this machine). Live load% is merged in
|
|
7
|
+
* on the Swift side from the daemon-warmed `.fleet-stats.json`; online/offline
|
|
8
|
+
* is deliberately NOT claimed here (the registry's cached tailscale flag is
|
|
9
|
+
* documented as stale in both directions — registry.ts isLikelyOnline).
|
|
10
|
+
*/
|
|
11
|
+
export interface MenubarDevice {
|
|
12
|
+
name: string;
|
|
13
|
+
platform: string;
|
|
14
|
+
interactive: boolean;
|
|
15
|
+
isLocal: boolean;
|
|
16
|
+
}
|
|
2
17
|
export interface MenubarSnapshot {
|
|
3
18
|
version: 1;
|
|
4
19
|
capturedAt: string;
|
|
5
20
|
routines: Record<string, unknown>[];
|
|
6
21
|
recentSessions: Record<string, unknown>[];
|
|
7
22
|
activeSessions: Record<string, unknown>[];
|
|
23
|
+
devices: MenubarDevice[];
|
|
8
24
|
watchdog: {
|
|
9
25
|
enabled: boolean;
|
|
10
26
|
lastTick: Pick<WatchdogTickResult, 'didNudge' | 'counts'> | null;
|
|
@@ -3,10 +3,29 @@ import * as path from 'path';
|
|
|
3
3
|
import { buildRoutineListJson } from '../../commands/routines.js';
|
|
4
4
|
import { backfillActiveRowsFromIndex, isRunningLiveSession, serializeActiveSessionsForJson, serializeSessionsJson } from '../../commands/sessions.js';
|
|
5
5
|
import { getConfigValue } from '../device-config.js';
|
|
6
|
+
import { loadDevices } from '../devices/registry.js';
|
|
6
7
|
import { machineId } from '../machine-id.js';
|
|
7
8
|
import { querySessions } from '../session/db.js';
|
|
8
9
|
import { readActiveSessionsCache } from '../session/session-cache.js';
|
|
9
10
|
import { getRuntimeStateDir } from '../state.js';
|
|
11
|
+
/**
|
|
12
|
+
* The full registered-device roster for the menu bar, from the local registry
|
|
13
|
+
* file only (no ssh, no stats probe) — as cheap as `buildRoutineListJson()`, so
|
|
14
|
+
* it rides the same 3-minute snapshot poll instead of a second timer.
|
|
15
|
+
*/
|
|
16
|
+
async function buildMenubarDevices() {
|
|
17
|
+
const reg = await loadDevices();
|
|
18
|
+
const interactiveHost = getConfigValue('interactive.host').value;
|
|
19
|
+
const self = machineId();
|
|
20
|
+
return Object.keys(reg)
|
|
21
|
+
.sort()
|
|
22
|
+
.map((name) => ({
|
|
23
|
+
name,
|
|
24
|
+
platform: reg[name].platform,
|
|
25
|
+
interactive: name === interactiveHost,
|
|
26
|
+
isLocal: name === self,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
10
29
|
export function readLastWatchdogTick(stateDir = path.join(getRuntimeStateDir(), 'watchdog')) {
|
|
11
30
|
try {
|
|
12
31
|
return JSON.parse(fs.readFileSync(path.join(stateDir, 'last-tick.json'), 'utf-8'));
|
|
@@ -17,9 +36,10 @@ export function readLastWatchdogTick(stateDir = path.join(getRuntimeStateDir(),
|
|
|
17
36
|
}
|
|
18
37
|
/** One-process read model for AGI Menu's repeating three-minute refresh. */
|
|
19
38
|
export async function computeMenubarSnapshot() {
|
|
20
|
-
const [routines, recent] = await Promise.all([
|
|
39
|
+
const [routines, recent, devices] = await Promise.all([
|
|
21
40
|
Promise.resolve(buildRoutineListJson()),
|
|
22
41
|
Promise.resolve(querySessions({ limit: 40, skipExistenceCheck: true })),
|
|
42
|
+
buildMenubarDevices(),
|
|
23
43
|
]);
|
|
24
44
|
const active = readActiveSessionsCache('local');
|
|
25
45
|
const rawSessions = active?.sessions ?? [];
|
|
@@ -42,6 +62,7 @@ export async function computeMenubarSnapshot() {
|
|
|
42
62
|
routines,
|
|
43
63
|
recentSessions: JSON.parse(serializeSessionsJson(recent)),
|
|
44
64
|
activeSessions: serializeActiveSessionsForJson(activeSessions),
|
|
65
|
+
devices,
|
|
45
66
|
watchdog: {
|
|
46
67
|
enabled: getConfigValue('watchdog.enabled').value === true,
|
|
47
68
|
lastTick: (() => {
|
package/dist/lib/migrate.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export declare function foldBrowserSessionsIntoProfiles(browserDir?: string): vo
|
|
|
77
77
|
* Params default to the real on-disk locations; they are injectable so tests
|
|
78
78
|
* can drive a fixture tree without touching the user's ~/.agents.
|
|
79
79
|
*/
|
|
80
|
-
export declare function repairSelfReferentialBinShims(versionsRoot?: string, shimsDir?: string): void;
|
|
80
|
+
export declare function repairSelfReferentialBinShims(versionsRoot?: string, shimsDir?: string, historyDir?: string): void;
|
|
81
81
|
/**
|
|
82
82
|
* Rename the legacy `extras-extras/` plugin-marketplace dir to `agents-extras/`
|
|
83
83
|
* inside every installed agent version-home, and rewrite cross-references in
|
package/dist/lib/migrate.js
CHANGED
|
@@ -14,6 +14,8 @@ import { machineId } from './machine-id.js';
|
|
|
14
14
|
import { AGENTS, agentConfigDirName, findInPath } from './agents.js';
|
|
15
15
|
import { createLink } from './platform/index.js';
|
|
16
16
|
import { migrateLegacyRoutineActivation, setJobEnabled } from './routines.js';
|
|
17
|
+
import { addEnabledRoutinesOnUpgrade } from './routine-activation.js';
|
|
18
|
+
import { DAEMON_TICK_ROUTINE_NAMES } from './daemon-ticks.js';
|
|
17
19
|
const HOME = process.env.HOME ?? os.homedir();
|
|
18
20
|
const USER_DIR = path.join(HOME, '.agents');
|
|
19
21
|
/** Canonical system-repo location (post-fold). */
|
|
@@ -825,7 +827,7 @@ function repairAgentConfigSymlinks() {
|
|
|
825
827
|
* Params default to the real on-disk locations; they are injectable so tests
|
|
826
828
|
* can drive a fixture tree without touching the user's ~/.agents.
|
|
827
829
|
*/
|
|
828
|
-
export function repairSelfReferentialBinShims(versionsRoot = path.join(HISTORY_DIR, 'versions'), shimsDir = path.resolve(CACHE_DIR, 'shims')) {
|
|
830
|
+
export function repairSelfReferentialBinShims(versionsRoot = path.join(HISTORY_DIR, 'versions'), shimsDir = path.resolve(CACHE_DIR, 'shims'), historyDir = path.dirname(versionsRoot)) {
|
|
829
831
|
// Normalize the shims dir through realpath so the prefix check below survives
|
|
830
832
|
// a symlinked ~/.agents (or macOS's /tmp -> /private/tmp): fs.realpathSync on
|
|
831
833
|
// the link target resolves those symlinks, so the dir we compare against must
|
|
@@ -884,7 +886,7 @@ export function repairSelfReferentialBinShims(versionsRoot = path.join(HISTORY_D
|
|
|
884
886
|
// Self-referential: the link resolves back into our own shims dir.
|
|
885
887
|
// findInPath does a pure-Node PATH scan (no subprocess) and already
|
|
886
888
|
// skips our shims dir, so it returns the genuine install if one exists.
|
|
887
|
-
const realBinary = findInPath(cli);
|
|
889
|
+
const realBinary = findInPath(cli, { shimsDir, historyDir });
|
|
888
890
|
try {
|
|
889
891
|
fs.unlinkSync(binLink);
|
|
890
892
|
// createLink: a real symlink where the OS allows it (POSIX, and Windows
|
|
@@ -2205,6 +2207,9 @@ export async function runMigration() {
|
|
|
2205
2207
|
// Rewrite routine YAML files: singular `device:` -> plural `devices: []`.
|
|
2206
2208
|
migrateRoutineDeviceToDevices();
|
|
2207
2209
|
migrateLegacyRoutineActivation();
|
|
2210
|
+
// These routines replace daemon timers that were always active. Devices with
|
|
2211
|
+
// an existing activation manifest must retain that behavior after upgrade.
|
|
2212
|
+
addEnabledRoutinesOnUpgrade(DAEMON_TICK_ROUTINE_NAMES);
|
|
2208
2213
|
// Fold the legacy watchdog enable sentinel into the watchdog routine so a user
|
|
2209
2214
|
// who opted in under the old build stays opted in after upgrading. After the
|
|
2210
2215
|
// routine rewrites above so the routines dir is in its canonical shape.
|
|
@@ -3,6 +3,8 @@ export declare function normalizeRoutineNames(names: Iterable<string>): string[]
|
|
|
3
3
|
export declare function enabledRoutineNames(): string[] | null;
|
|
4
4
|
export declare function routineEnabledOnThisDevice(name: string): boolean | null;
|
|
5
5
|
export declare function replaceEnabledRoutines(names: Iterable<string>): string[];
|
|
6
|
+
/** Add newly introduced replacements to an already-materialized device manifest. */
|
|
7
|
+
export declare function addEnabledRoutinesOnUpgrade(names: Iterable<string>): boolean;
|
|
6
8
|
/**
|
|
7
9
|
* Add or remove one routine on this machine. `legacyEnabledNames` seeds the
|
|
8
10
|
* manifest the first time an upgraded host changes activation, preserving every
|
|
@@ -26,6 +26,22 @@ export function replaceEnabledRoutines(names) {
|
|
|
26
26
|
updateMeta((meta) => ({ ...meta, deviceRoutines: normalized }));
|
|
27
27
|
return normalized;
|
|
28
28
|
}
|
|
29
|
+
/** Add newly introduced replacements to an already-materialized device manifest. */
|
|
30
|
+
export function addEnabledRoutinesOnUpgrade(names) {
|
|
31
|
+
const additions = normalizeRoutineNames(names);
|
|
32
|
+
let changed = false;
|
|
33
|
+
updateMeta((meta) => {
|
|
34
|
+
if (!Array.isArray(meta.deviceRoutines))
|
|
35
|
+
return meta;
|
|
36
|
+
const current = normalizeRoutineNames(meta.deviceRoutines);
|
|
37
|
+
const next = normalizeRoutineNames([...current, ...additions]);
|
|
38
|
+
if (next.length === current.length && next.every((name, index) => name === current[index]))
|
|
39
|
+
return meta;
|
|
40
|
+
changed = true;
|
|
41
|
+
return { ...meta, deviceRoutines: next };
|
|
42
|
+
});
|
|
43
|
+
return changed;
|
|
44
|
+
}
|
|
29
45
|
/**
|
|
30
46
|
* Add or remove one routine on this machine. `legacyEnabledNames` seeds the
|
|
31
47
|
* manifest the first time an upgraded host changes activation, preserving every
|
package/dist/lib/runner.d.ts
CHANGED
|
@@ -59,6 +59,13 @@ export interface RoutineLaunchPlan {
|
|
|
59
59
|
*/
|
|
60
60
|
export declare function resolveRoutineLaunch(config: JobConfig, cwd?: string, deps?: {
|
|
61
61
|
resolveRunVersion?: typeof resolveRunVersion;
|
|
62
|
+
readAccountLabels?: () => {
|
|
63
|
+
labels: Record<string, {
|
|
64
|
+
agent: AgentId;
|
|
65
|
+
fingerprint: string;
|
|
66
|
+
}>;
|
|
67
|
+
};
|
|
68
|
+
resolveAccountLabel?: (agent: AgentId, label: string) => Promise<string>;
|
|
62
69
|
}): Promise<RoutineLaunchPlan>;
|
|
63
70
|
/**
|
|
64
71
|
* Rewrite `cmd[0]` to the absolute binary for `agent@version` when installed.
|
|
@@ -105,5 +112,16 @@ export declare function inferFinalStatusFromLog(stdoutPath: string, agent: Agent
|
|
|
105
112
|
status: 'completed' | 'failed';
|
|
106
113
|
exitCode: number;
|
|
107
114
|
} | null;
|
|
115
|
+
/**
|
|
116
|
+
* PIDs of the in-flight detached routine children on THIS device — every local
|
|
117
|
+
* run record still marked `running` whose spawned process is genuinely alive
|
|
118
|
+
* (`isPidOurs`, so a dead-and-reused pid does not count). These are the `unref`'d
|
|
119
|
+
* spawns that survive a daemon exit in their own process group (SING-11a): a
|
|
120
|
+
* takeover must not kill them and `stopDaemon` reports them rather than pretending
|
|
121
|
+
* the process tree is clean (SING-12). Scoped to `getRunsDir()`, which is under
|
|
122
|
+
* this state dir's HOME, so a different state dir's children are invisible here.
|
|
123
|
+
* `host:`-placed runs have no local pid and are excluded.
|
|
124
|
+
*/
|
|
125
|
+
export declare function listLiveRoutineChildren(): number[];
|
|
108
126
|
/** Scan all runs marked "running" and finalize any whose process has exited. */
|
|
109
127
|
export declare function monitorRunningJobs(): void;
|
package/dist/lib/runner.js
CHANGED
|
@@ -513,6 +513,11 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd(), deps = {
|
|
|
513
513
|
// credential (RUSH-1957). Falls through to the strategy only when the account
|
|
514
514
|
// is not signed in on this box, with a loud warning rather than a silent stall.
|
|
515
515
|
if (config.account) {
|
|
516
|
+
const { readAccountLabels, resolveAccountLabel } = await import('./account-labels.js');
|
|
517
|
+
if ((deps.readAccountLabels ?? readAccountLabels)().labels[config.account]) {
|
|
518
|
+
const version = await (deps.resolveAccountLabel ?? resolveAccountLabel)(agent, config.account);
|
|
519
|
+
return { chain: [{ agent, version }], rotation: null, pinned: true };
|
|
520
|
+
}
|
|
516
521
|
const version = await resolveAccountVersion(agent, config.account);
|
|
517
522
|
if (version) {
|
|
518
523
|
return { chain: [{ agent, version }], rotation: null, pinned: true };
|
|
@@ -1720,6 +1725,53 @@ function finalizeHostRun(meta) {
|
|
|
1720
1725
|
}
|
|
1721
1726
|
catch { /* unreachable host or unreadable sidecar — retry next sweep */ }
|
|
1722
1727
|
}
|
|
1728
|
+
/**
|
|
1729
|
+
* PIDs of the in-flight detached routine children on THIS device — every local
|
|
1730
|
+
* run record still marked `running` whose spawned process is genuinely alive
|
|
1731
|
+
* (`isPidOurs`, so a dead-and-reused pid does not count). These are the `unref`'d
|
|
1732
|
+
* spawns that survive a daemon exit in their own process group (SING-11a): a
|
|
1733
|
+
* takeover must not kill them and `stopDaemon` reports them rather than pretending
|
|
1734
|
+
* the process tree is clean (SING-12). Scoped to `getRunsDir()`, which is under
|
|
1735
|
+
* this state dir's HOME, so a different state dir's children are invisible here.
|
|
1736
|
+
* `host:`-placed runs have no local pid and are excluded.
|
|
1737
|
+
*/
|
|
1738
|
+
export function listLiveRoutineChildren() {
|
|
1739
|
+
const runsDir = getRunsDir();
|
|
1740
|
+
if (!fs.existsSync(runsDir))
|
|
1741
|
+
return [];
|
|
1742
|
+
const pids = [];
|
|
1743
|
+
let jobDirs;
|
|
1744
|
+
try {
|
|
1745
|
+
jobDirs = fs.readdirSync(runsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
1746
|
+
}
|
|
1747
|
+
catch {
|
|
1748
|
+
return pids;
|
|
1749
|
+
}
|
|
1750
|
+
for (const jobDir of jobDirs) {
|
|
1751
|
+
const jobRunsPath = path.join(runsDir, jobDir.name);
|
|
1752
|
+
let runDirs;
|
|
1753
|
+
try {
|
|
1754
|
+
runDirs = fs.readdirSync(jobRunsPath, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
1755
|
+
}
|
|
1756
|
+
catch {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
for (const runDirEntry of runDirs) {
|
|
1760
|
+
const metaPath = path.join(jobRunsPath, runDirEntry.name, 'meta.json');
|
|
1761
|
+
if (!fs.existsSync(metaPath))
|
|
1762
|
+
continue;
|
|
1763
|
+
try {
|
|
1764
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
1765
|
+
if (meta.status !== 'running' || meta.hostTaskId || !meta.pid)
|
|
1766
|
+
continue;
|
|
1767
|
+
if (isPidOurs(meta.pid, meta.spawnedAt))
|
|
1768
|
+
pids.push(meta.pid);
|
|
1769
|
+
}
|
|
1770
|
+
catch { /* unreadable/partial record — skip */ }
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
return pids;
|
|
1774
|
+
}
|
|
1723
1775
|
/** Scan all runs marked "running" and finalize any whose process has exited. */
|
|
1724
1776
|
export function monitorRunningJobs() {
|
|
1725
1777
|
const runsDir = getRunsDir();
|
|
Binary file
|
|
Binary file
|
|
@@ -67,6 +67,23 @@ export declare function shouldSelfHealForUpgrade(persistent: boolean, storeSize:
|
|
|
67
67
|
* keeps serving; its own sweep adopts the new code at the next quiet moment.
|
|
68
68
|
*/
|
|
69
69
|
export declare function shouldTeardownVersionSkewedBroker(realHeldBundles: number): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Whether a version-skewed client may evict the reachable broker at all. The
|
|
72
|
+
* held-bundle gate above is necessary but not sufficient: a broker the always-on
|
|
73
|
+
* daemon is hosting must NEVER be client-evicted, even when it holds zero
|
|
74
|
+
* unlocks. teardownStaleBroker() recognizes only the standalone broker's
|
|
75
|
+
* pidPath() O_EXCL claim (the daemon writes ownerPath(), never pidPath()), so
|
|
76
|
+
* evicting a daemon-hosted broker unlinks its socket WITHOUT stopping the daemon;
|
|
77
|
+
* the daemon then keeps hostedBroker != null and shouldTakeOverBroker() refuses
|
|
78
|
+
* to re-host, orphaning its broker until the daemon restarts while every reader
|
|
79
|
+
* falls onto cold one-off brokers that re-prompt Touch ID — the storm. Deferring
|
|
80
|
+
* is safe: daemon code-version upgrades are handled by postinstall.js restarting
|
|
81
|
+
* it, and agentPing() already gated on PROTOCOL_VERSION, so a code-skewed daemon
|
|
82
|
+
* broker is still wire-compatible. Only when NO daemon owns the broker (churning
|
|
83
|
+
* dev installs with a dead/absent daemon — the case #435's client twin was built
|
|
84
|
+
* for) does the zero-held-bundles teardown apply, exactly as before.
|
|
85
|
+
*/
|
|
86
|
+
export declare function shouldClientEvictSkewedBroker(daemonRunning: boolean, realHeldBundles: number): boolean;
|
|
70
87
|
export interface StoredBundle {
|
|
71
88
|
bundle: SecretsBundle;
|
|
72
89
|
env: Record<string, string>;
|
|
@@ -84,6 +101,8 @@ export interface AgentStatusEntry {
|
|
|
84
101
|
leaseId?: string;
|
|
85
102
|
keys?: string[];
|
|
86
103
|
}
|
|
104
|
+
/** Public accessor for the broker's socket path — `agents daemon status`/`services` reads it for display. */
|
|
105
|
+
export declare function secretsBrokerSocketPath(): string;
|
|
87
106
|
/**
|
|
88
107
|
* Read the current broker capability token, or null if none is present. Clients
|
|
89
108
|
* read it fresh per request and attach it to every non-ping command; a broker
|