@phnx-labs/agents-cli 1.22.57 → 1.22.59
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 +294 -0
- package/README.md +29 -0
- package/dist/bootstrap.js +39 -1
- package/dist/commands/accounts.js +7 -3
- package/dist/commands/apply.js +10 -2
- package/dist/commands/fork.d.ts +23 -10
- package/dist/commands/fork.js +115 -58
- package/dist/commands/monitors.js +198 -23
- package/dist/commands/prune.js +5 -3
- package/dist/commands/routines.d.ts +8 -0
- package/dist/commands/routines.js +57 -3
- package/dist/commands/routines.test-fixture.js +5 -0
- package/dist/commands/send.d.ts +2 -1
- package/dist/commands/send.js +7 -5
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions-stats.js +37 -5
- package/dist/commands/sessions.js +40 -5
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/ssh.js +12 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.js +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -1
- package/dist/lib/account-registry.d.ts +5 -1
- package/dist/lib/account-registry.js +47 -14
- package/dist/lib/accounting/capacity.d.ts +18 -7
- package/dist/lib/accounting/capacity.js +19 -8
- package/dist/lib/accounting/usage-sync.d.ts +29 -1
- package/dist/lib/accounting/usage-sync.js +76 -2
- package/dist/lib/accounting/usage.js +7 -1
- package/dist/lib/auth-mint.d.ts +11 -1
- package/dist/lib/auth-mint.js +21 -6
- package/dist/lib/auto-pull-worker.js +7 -2
- package/dist/lib/browser/ipc.d.ts +8 -0
- package/dist/lib/browser/ipc.js +87 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +96 -11
- package/dist/lib/browser/sessions-list.js +10 -1
- package/dist/lib/cloud/rush.d.ts +7 -0
- package/dist/lib/cloud/rush.js +29 -1
- package/dist/lib/daemon/daemon.d.ts +22 -0
- package/dist/lib/daemon/daemon.js +39 -0
- package/dist/lib/daemon/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +86 -45
- package/dist/lib/daemon/session-index-service.js +9 -1
- package/dist/lib/daemon/usage-sync-service.d.ts +3 -3
- package/dist/lib/daemon/usage-sync-service.js +14 -8
- package/dist/lib/daemon-services.js +1 -1
- package/dist/lib/daemon-ticks.d.ts +15 -0
- package/dist/lib/daemon-ticks.js +26 -0
- package/dist/lib/device-config.d.ts +5 -1
- package/dist/lib/device-config.js +2 -2
- package/dist/lib/devices/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- package/dist/lib/devices/health.js +5 -1
- package/dist/lib/devices/pool.d.ts +25 -2
- package/dist/lib/devices/pool.js +32 -2
- package/dist/lib/devices/stats-cache.d.ts +0 -6
- package/dist/lib/devices/stats-cache.js +2 -9
- package/dist/lib/doctor-diff.d.ts +14 -0
- package/dist/lib/doctor-diff.js +120 -9
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/git.d.ts +38 -0
- package/dist/lib/git.js +58 -0
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/ready.d.ts +8 -0
- package/dist/lib/hosts/ready.js +13 -2
- package/dist/lib/hosts/reconnect.d.ts +52 -203
- package/dist/lib/hosts/reconnect.js +64 -284
- package/dist/lib/installations/migrate.d.ts +6 -120
- package/dist/lib/installations/migrate.js +27 -259
- package/dist/lib/installations/shims.d.ts +13 -95
- package/dist/lib/installations/shims.js +22 -139
- package/dist/lib/installations/store.js +1 -1
- package/dist/lib/installations/versions.d.ts +43 -133
- package/dist/lib/installations/versions.js +94 -206
- package/dist/lib/monitors/config.d.ts +71 -3
- package/dist/lib/monitors/config.js +100 -12
- package/dist/lib/monitors/pid-watch.d.ts +35 -0
- package/dist/lib/monitors/pid-watch.js +45 -0
- package/dist/lib/monitors/remote.d.ts +18 -0
- package/dist/lib/monitors/remote.js +11 -0
- package/dist/lib/permissions.js +7 -2
- package/dist/lib/plugins/plugins.d.ts +17 -3
- package/dist/lib/plugins/plugins.js +84 -9
- package/dist/lib/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- package/dist/lib/pty-server.d.ts +14 -0
- package/dist/lib/pty-server.js +49 -5
- package/dist/lib/refresh.d.ts +9 -0
- package/dist/lib/refresh.js +3 -1
- package/dist/lib/routine-readiness.d.ts +15 -1
- package/dist/lib/routine-readiness.js +41 -0
- package/dist/lib/sandbox.d.ts +4 -1
- package/dist/lib/sandbox.js +30 -1
- package/dist/lib/secrets/agent.d.ts +80 -225
- package/dist/lib/secrets/agent.js +139 -401
- package/dist/lib/secrets/bundles.d.ts +73 -222
- package/dist/lib/secrets/bundles.js +168 -467
- package/dist/lib/secrets/drivers/rush.js +5 -0
- package/dist/lib/secrets/reaper.d.ts +28 -70
- package/dist/lib/secrets/reaper.js +30 -85
- package/dist/lib/secrets/remote.d.ts +42 -129
- package/dist/lib/secrets/remote.js +55 -173
- package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
- package/dist/lib/self-heal/checks/install-staging.js +96 -0
- package/dist/lib/self-heal/registry.js +2 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/self-update.d.ts +65 -0
- package/dist/lib/self-update.js +138 -0
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/cloud.js +5 -0
- package/dist/lib/session/db.d.ts +51 -6
- package/dist/lib/session/db.js +266 -20
- package/dist/lib/session/fork.d.ts +45 -26
- package/dist/lib/session/fork.js +32 -95
- package/dist/lib/session/tool-calls.d.ts +43 -1
- package/dist/lib/session/tool-calls.js +74 -44
- package/dist/lib/session/tool-store.d.ts +33 -2
- package/dist/lib/session/tool-store.js +56 -3
- package/dist/lib/smart-launch.d.ts +6 -0
- package/dist/lib/smart-launch.js +5 -2
- package/dist/lib/staleness/writers/plugins.js +5 -2
- package/dist/lib/staleness/writers/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- package/dist/lib/staleness/writers/subagents.js +13 -3
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +7 -4
- package/dist/lib/subagents.js +8 -2
- package/dist/lib/sync-status.d.ts +22 -0
- package/dist/lib/sync-status.js +27 -0
- package/dist/lib/sync-umbrella.d.ts +9 -0
- package/dist/lib/sync-umbrella.js +21 -2
- package/dist/lib/teams/scheduler.d.ts +10 -0
- package/dist/lib/teams/scheduler.js +8 -0
- package/dist/lib/traces/insights.d.ts +47 -14
- package/dist/lib/traces/insights.js +92 -21
- package/dist/lib/traces/phenotype.d.ts +23 -3
- package/dist/lib/traces/phenotype.js +72 -24
- package/dist/lib/traces/sync.d.ts +128 -6
- package/dist/lib/traces/sync.js +294 -35
- package/dist/lib/traces/worker-template.js +154 -1
- package/dist/lib/view-types.d.ts +12 -0
- package/package.json +2 -2
|
@@ -21,7 +21,7 @@ import { getCliLaunch, getAgentsBinDir } from '../cli-entry.js';
|
|
|
21
21
|
import { resolveJobPrompt, parseTimeout, writeRunMeta, listRuns, getJobRunsDir, getRunDir, checkJobDeviceEligibility, finalizeRunMeta, resolveJobExecutionContext, slotRunId, claimRunSlot, readRunMeta, resolveHostStrategy, } from '../scheduling/routines.js';
|
|
22
22
|
import { getRunsDir, getUserAgentsDir, readMeta, getDaemonDir } from '../state.js';
|
|
23
23
|
import { shortCodexHome } from '../codex-home.js';
|
|
24
|
-
import { prepareJobHome, buildSpawnEnv, getJobHomePath, assertSandboxForwardsHostGhAuth } from '../sandbox.js';
|
|
24
|
+
import { prepareJobHome, buildSpawnEnv, getJobHomePath, assertSandboxForwardsHostGhAuth, linkVersionAuth } from '../sandbox.js';
|
|
25
25
|
import { resolveModel, buildReasoningFlags } from '../models.js';
|
|
26
26
|
import { createTimer, redactPrompt, emitRoutineEnd } from '../feed/events.js';
|
|
27
27
|
import { resolveHarnessAdapter } from '../harness/index.js';
|
|
@@ -37,8 +37,6 @@ import { walkForFiles } from '../fs-walk.js';
|
|
|
37
37
|
import { getBinaryPath, isVersionInstalled, resolveVersion, getVersionHomePath } from '../installations/versions.js';
|
|
38
38
|
import { resolveClaudeSetupToken } from '../claude-account-token.js';
|
|
39
39
|
import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, } from '../accounting/rotate.js';
|
|
40
|
-
import { readAuthHealth, isDeadVerdict } from '../auth-health.js';
|
|
41
|
-
import { machineId } from '../machine-id.js';
|
|
42
40
|
import { isHeadedDeviceRole, selfConfiguredDeviceRole } from '../device-config.js';
|
|
43
41
|
import { isSelfUpdatingAgent, ROUTINE_AGENT_IDS, isAgentHardDeprecated, hardDeprecationError } from '../agents.js';
|
|
44
42
|
import { isCustomHarnessName, readProfile } from '../profiles.js';
|
|
@@ -860,6 +858,27 @@ function readCommandExitCode(runDir) {
|
|
|
860
858
|
return null;
|
|
861
859
|
}
|
|
862
860
|
}
|
|
861
|
+
/** Claude's own local auth check; unlike account metadata it cannot mistake identity for a usable login. */
|
|
862
|
+
export function claudeVersionIsAuthenticated(version) {
|
|
863
|
+
if (!isVersionInstalled('claude', version))
|
|
864
|
+
return true; // synthetic/unit plans are validated at spawn
|
|
865
|
+
const binary = getBinaryPath('claude', version);
|
|
866
|
+
if (!binary)
|
|
867
|
+
return false;
|
|
868
|
+
const home = getVersionHomePath('claude', version);
|
|
869
|
+
try {
|
|
870
|
+
const raw = execFileSync(binary, ['auth', 'status', '--json'], {
|
|
871
|
+
encoding: 'utf8',
|
|
872
|
+
timeout: 5_000,
|
|
873
|
+
env: { ...process.env, HOME: process.env.AGENTS_REAL_HOME || os.homedir(), CLAUDE_CONFIG_DIR: path.join(home, '.claude') },
|
|
874
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
875
|
+
});
|
|
876
|
+
return JSON.parse(raw).loggedIn === true;
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
863
882
|
/**
|
|
864
883
|
* Resolve the version/account chain for a routine the same way `agents run`
|
|
865
884
|
* does: honor an explicit `version:` pin; otherwise use the configured run
|
|
@@ -890,7 +909,7 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd(), deps = {
|
|
|
890
909
|
: false;
|
|
891
910
|
const selectedCredential = config.account
|
|
892
911
|
? (explicitCredential ? config.account : undefined)
|
|
893
|
-
: resolveAccountSelection(undefined, agent, meta);
|
|
912
|
+
: resolveAccountSelection(undefined, agent, meta)?.id;
|
|
894
913
|
if (selectedCredential) {
|
|
895
914
|
// A default/binding may resolve to a native account — never route that
|
|
896
915
|
// through the provider credential path (it has no bundle to resolve).
|
|
@@ -980,6 +999,24 @@ export async function resolveRoutineLaunch(config, cwd = process.cwd(), deps = {
|
|
|
980
999
|
if (!version) {
|
|
981
1000
|
version = resolveVersion(agent, cwd) ?? undefined;
|
|
982
1001
|
}
|
|
1002
|
+
if (agent === 'claude' && version) {
|
|
1003
|
+
const checkClaudeAuth = deps.claudeVersionIsAuthenticated ?? claudeVersionIsAuthenticated;
|
|
1004
|
+
const authenticatedHealthy = rotation?.healthy.filter((candidate) => checkClaudeAuth(candidate.version));
|
|
1005
|
+
const authenticated = authenticatedHealthy?.find((candidate) => candidate.version === version) ?? authenticatedHealthy?.[0];
|
|
1006
|
+
if (rotation && (!authenticated || authenticatedHealthy.length === 0)) {
|
|
1007
|
+
throw new Error(`Routine '${config.name}' found no authenticated Claude account; run \`claude /login\` for an installed version or pin a provider profile.`);
|
|
1008
|
+
}
|
|
1009
|
+
if (!rotation && !checkClaudeAuth(version)) {
|
|
1010
|
+
throw new Error(`Routine '${config.name}' found no authenticated Claude account at claude@${version}; run \`claude /login\` for that version or remove the pin.`);
|
|
1011
|
+
}
|
|
1012
|
+
if (rotation && authenticated) {
|
|
1013
|
+
const rejected = rotation.healthy.filter((candidate) => !authenticatedHealthy.includes(candidate));
|
|
1014
|
+
if (rejected.length > 0 || rotation.picked !== authenticated) {
|
|
1015
|
+
rotation = { ...rotation, picked: authenticated, healthy: authenticatedHealthy, excluded: [...rotation.excluded, ...rejected] };
|
|
1016
|
+
}
|
|
1017
|
+
version = authenticated.version;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
983
1020
|
if (!version) {
|
|
984
1021
|
process.stderr.write(`[agents] routine ${config.name}: no version of ${agent} configured — ` +
|
|
985
1022
|
`run: agents add ${agent}@<version> && agents use ${agent} <version>\n`);
|
|
@@ -1143,6 +1180,10 @@ export function buildRoutineSpawnEnv(baseEnv, agent, version, timezone, overlayH
|
|
|
1143
1180
|
// to the overlay so an ambient value cannot bypass the routine sandbox.
|
|
1144
1181
|
out.XDG_CONFIG_HOME = path.join(overlayHome, '.config');
|
|
1145
1182
|
}
|
|
1183
|
+
if (overlayHome && agent === 'claude')
|
|
1184
|
+
out.HOME = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
1185
|
+
if (overlayHome && agent === 'codex')
|
|
1186
|
+
out.CODEX_HOME = path.join(overlayHome, '.codex');
|
|
1146
1187
|
if (timezone)
|
|
1147
1188
|
out.TZ = timezone;
|
|
1148
1189
|
return out;
|
|
@@ -1304,7 +1345,7 @@ async function executeJobPlaced(config, deps, attempt) {
|
|
|
1304
1345
|
// sentinel, version homes) from HOME, which the overlay would hide. Exec
|
|
1305
1346
|
// still isolates the run in the host version home it swaps HOME into.
|
|
1306
1347
|
const useSandbox = config.sandbox !== false && !config.resume && !(config.agent && isCustomHarnessName(config.agent));
|
|
1307
|
-
const overlayHome = useSandbox ? prepareJobHome(config) : undefined;
|
|
1348
|
+
const overlayHome = useSandbox ? prepareJobHome(config, primaryVersion) : undefined;
|
|
1308
1349
|
const runId = attempt.runId;
|
|
1309
1350
|
const runDir = getRunDir(config.name, runId);
|
|
1310
1351
|
fs.mkdirSync(runDir, { recursive: true });
|
|
@@ -1329,7 +1370,7 @@ async function executeJobPlaced(config, deps, attempt) {
|
|
|
1329
1370
|
if (!dispatchesViaAgentsRun(config)) {
|
|
1330
1371
|
const { findUnifiedAccount, resolveAccountSelection, resolveCredentialAccount } = await import('../account-registry.js');
|
|
1331
1372
|
const meta = readMeta();
|
|
1332
|
-
const selectedAccount = resolveAccountSelection(config.account, effectiveAgent, meta);
|
|
1373
|
+
const selectedAccount = resolveAccountSelection(config.account, effectiveAgent, meta)?.id;
|
|
1333
1374
|
if (selectedAccount) {
|
|
1334
1375
|
// Only a provider account injects env; a native account (explicit or via a
|
|
1335
1376
|
// device-scoped binding) is read from the harness home and forwards nothing.
|
|
@@ -1360,24 +1401,24 @@ async function executeJobPlaced(config, deps, attempt) {
|
|
|
1360
1401
|
// Baseline the child's transcript dirs BEFORE spawning so archiveRoutineTranscripts
|
|
1361
1402
|
// copies only THIS run's transcript out of a shared per-version home (RUSH-2271).
|
|
1362
1403
|
snapshotRoutineTranscriptBase(meta, runDir, overlayHome);
|
|
1363
|
-
// Auth preflight: if the
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1404
|
+
// Auth preflight: if the resolved account is provably signed out (revoked or
|
|
1405
|
+
// unconfigured), record a terminal `blocked`/`agent_auth_failed` run with the
|
|
1406
|
+
// re-login repair instead of spawning a doomed run that 401s and burns a
|
|
1407
|
+
// session (PHNX-3415). `blocked` (readiness rejection, no body ran) is kept
|
|
1408
|
+
// distinct from `failed` (a body ran and errored) per RT-7. Cache-only + fails
|
|
1409
|
+
// OPEN — see fireTimeAuthReadiness.
|
|
1369
1410
|
const preflightVersion = launch.chain[0]?.version;
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
}
|
|
1411
|
+
// Dynamic import breaks the routine-readiness -> scheduling/routines -> runner cycle.
|
|
1412
|
+
const { fireTimeAuthReadiness } = await import('../routine-readiness.js');
|
|
1413
|
+
const authBlocker = preflightVersion ? fireTimeAuthReadiness(effectiveAgent, preflightVersion) : null;
|
|
1414
|
+
if (authBlocker) {
|
|
1415
|
+
process.stderr.write(`[agents] routine ${config.name}: ${effectiveAgent}@${preflightVersion} ${authBlocker.code} — skipping run (${authBlocker.repair})\n`);
|
|
1416
|
+
meta.readiness = authBlocker;
|
|
1417
|
+
finalizeRunMeta(meta, 'blocked', null, { errorMessage: `blocked: ${authBlocker.code} — ${authBlocker.message}` });
|
|
1418
|
+
writeRunMeta(meta);
|
|
1419
|
+
timer.end({ status: 'blocked', runId, error: authBlocker.code });
|
|
1420
|
+
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
1421
|
+
return { meta, reportPath: null };
|
|
1381
1422
|
}
|
|
1382
1423
|
const timeoutMs = parseTimeout(config.timeout) || 10 * 60 * 1000;
|
|
1383
1424
|
// Loop path: delegate to runLoop (same driver as `agents run --loop` / workflow loop:).
|
|
@@ -1444,6 +1485,8 @@ async function executeJobPlaced(config, deps, attempt) {
|
|
|
1444
1485
|
meta.version = attemptVersion;
|
|
1445
1486
|
snapshotRoutineTranscriptBase(meta, runDir, overlayHome);
|
|
1446
1487
|
const viaAgentsRun = dispatchesViaAgentsRun(config);
|
|
1488
|
+
if (overlayHome)
|
|
1489
|
+
linkVersionAuth(overlayHome, attemptAgent, attemptVersion);
|
|
1447
1490
|
const cmd = viaAgentsRun
|
|
1448
1491
|
? baseCmd
|
|
1449
1492
|
: pinJobBinary(baseCmd, attemptAgent, attemptVersion);
|
|
@@ -1837,7 +1880,7 @@ async function executeJobDetachedClaimed(config, attempt, hooks) {
|
|
|
1837
1880
|
// sentinel, version homes) from HOME, which the overlay would hide. Exec
|
|
1838
1881
|
// still isolates the run in the host version home it swaps HOME into.
|
|
1839
1882
|
const useSandbox = config.sandbox !== false && !config.resume && !(config.agent && isCustomHarnessName(config.agent));
|
|
1840
|
-
const overlayHome = useSandbox ? prepareJobHome(config) : undefined;
|
|
1883
|
+
const overlayHome = useSandbox ? prepareJobHome(config, version) : undefined;
|
|
1841
1884
|
const runId = attempt.runId;
|
|
1842
1885
|
const runDir = getRunDir(config.name, runId);
|
|
1843
1886
|
fs.mkdirSync(runDir, { recursive: true });
|
|
@@ -1850,7 +1893,7 @@ async function executeJobDetachedClaimed(config, attempt, hooks) {
|
|
|
1850
1893
|
const { findAccount, resolveAccountSelection, resolveCredentialAccount } = await import('../account-registry.js');
|
|
1851
1894
|
const selectedAccount = config.account && !findAccount(config.account)
|
|
1852
1895
|
? undefined
|
|
1853
|
-
: resolveAccountSelection(config.account, config.agent, readMeta());
|
|
1896
|
+
: resolveAccountSelection(config.account, config.agent, readMeta())?.id;
|
|
1854
1897
|
if (selectedAccount)
|
|
1855
1898
|
Object.assign(baseEnv, resolveCredentialAccount(selectedAccount, config.agent).env);
|
|
1856
1899
|
}
|
|
@@ -1891,28 +1934,26 @@ async function executeJobDetachedClaimed(config, attempt, hooks) {
|
|
|
1891
1934
|
// Baseline the child's transcript dirs BEFORE spawning so archiveRoutineTranscripts
|
|
1892
1935
|
// copies only THIS run's transcript out of a shared per-version home (RUSH-2271).
|
|
1893
1936
|
snapshotRoutineTranscriptBase(meta, runDir, overlayHome);
|
|
1894
|
-
// Auth preflight (mirrors executeJob):
|
|
1895
|
-
//
|
|
1896
|
-
//
|
|
1897
|
-
//
|
|
1898
|
-
// doomed run + poisoned report. Cache-only; fails OPEN on any non-dead/missing
|
|
1899
|
-
// verdict, and agents with no live probe (codex/gemini/grok) are never blocked.
|
|
1937
|
+
// Auth preflight (mirrors executeJob): a provably signed-out resolved account
|
|
1938
|
+
// records a terminal `blocked`/`agent_auth_failed` run with the re-login repair
|
|
1939
|
+
// instead of spawning a doomed run that 401s and burns a session (PHNX-3415).
|
|
1940
|
+
// Cache-only + fails OPEN — see fireTimeAuthReadiness.
|
|
1900
1941
|
const preflightVersion = launch.chain[0]?.version;
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
}
|
|
1909
|
-
catch { /* already closed */ }
|
|
1910
|
-
finalizeRunMeta(meta, 'failed', 1, { errorMessage: reason });
|
|
1911
|
-
writeRunMeta(meta);
|
|
1912
|
-
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
1913
|
-
timer.end({ status: 'failed', exitCode: 1, runId, error: reason });
|
|
1914
|
-
return meta;
|
|
1942
|
+
// Dynamic import breaks the routine-readiness -> scheduling/routines -> runner cycle.
|
|
1943
|
+
const { fireTimeAuthReadiness } = await import('../routine-readiness.js');
|
|
1944
|
+
const authBlocker = preflightVersion ? fireTimeAuthReadiness(effectiveAgent, preflightVersion) : null;
|
|
1945
|
+
if (authBlocker) {
|
|
1946
|
+
process.stderr.write(`[agents] routine ${config.name}: ${effectiveAgent}@${preflightVersion} ${authBlocker.code} — skipping run (${authBlocker.repair})\n`);
|
|
1947
|
+
try {
|
|
1948
|
+
fs.closeSync(stdoutFd);
|
|
1915
1949
|
}
|
|
1950
|
+
catch { /* already closed */ }
|
|
1951
|
+
meta.readiness = authBlocker;
|
|
1952
|
+
finalizeRunMeta(meta, 'blocked', null, { errorMessage: `blocked: ${authBlocker.code} — ${authBlocker.message}` });
|
|
1953
|
+
writeRunMeta(meta);
|
|
1954
|
+
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
1955
|
+
timer.end({ status: 'blocked', runId, error: authBlocker.code });
|
|
1956
|
+
return meta;
|
|
1916
1957
|
}
|
|
1917
1958
|
const child = spawn(cmd[0], cmd.slice(1), {
|
|
1918
1959
|
stdio: ['ignore', stdoutFd, stdoutFd],
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* and deadline instead.
|
|
12
12
|
*/
|
|
13
13
|
import { BasePeriodicService } from './service.js';
|
|
14
|
-
import { runSessionIndexWarmTick } from '../daemon-ticks.js';
|
|
14
|
+
import { runDeferredToolIndex, runSessionIndexWarmTick } from '../daemon-ticks.js';
|
|
15
15
|
/** Matches the historical inline interval (daemon.ts SESSION_INDEX_WARM_TICK_MS). */
|
|
16
16
|
const SESSION_INDEX_WARM_TICK_MS = 20_000;
|
|
17
17
|
/** Hard cap per tick — well above a healthy incremental scan, short enough that a hang never freezes the service for long. */
|
|
@@ -36,5 +36,13 @@ export class SessionIndexService extends BasePeriodicService {
|
|
|
36
36
|
ctx.log('INFO', 'session-index warm: skipped, another process holds the scan claim');
|
|
37
37
|
else if (indexed > 0)
|
|
38
38
|
ctx.log('INFO', `session-index warm: indexed ${indexed} transcript(s)`);
|
|
39
|
+
// Deferred tool-index pass (PHNX-3411): fill tool_scan_ledger rows for
|
|
40
|
+
// harnesses whose scanner produces no events. Runs only when the warm tick
|
|
41
|
+
// claimed the scan lock (i.e. we are the active indexer this tick).
|
|
42
|
+
if (claimed) {
|
|
43
|
+
const { indexed: toolIndexed } = await runDeferredToolIndex();
|
|
44
|
+
if (toolIndexed > 0)
|
|
45
|
+
ctx.log('INFO', `session-index deferred: tool-indexed ${toolIndexed} transcript(s)`);
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
48
|
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Fleet usage-snapshot sync as a `PeriodicService` (PHNX-3392 usage-sync).
|
|
3
3
|
*
|
|
4
4
|
* On a headed box (personal/desktop) each tick pushes the local identity-keyed
|
|
5
|
-
* Claude usage rows to worker peers that cannot read usage themselves.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Claude usage rows to worker peers that cannot read usage themselves. On a
|
|
6
|
+
* worker whose cache is empty or stale, the same tick pulls those rows from the
|
|
7
|
+
* primary headed device. Both drivers gate on the self role, so this service is
|
|
8
8
|
* safe to register everywhere. Single-executor per destination: each daemon only
|
|
9
9
|
* writes the DESTINATION's own cache, and the merge is newest-wins + idempotent.
|
|
10
10
|
*/
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Fleet usage-snapshot sync as a `PeriodicService` (PHNX-3392 usage-sync).
|
|
3
3
|
*
|
|
4
4
|
* On a headed box (personal/desktop) each tick pushes the local identity-keyed
|
|
5
|
-
* Claude usage rows to worker peers that cannot read usage themselves.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Claude usage rows to worker peers that cannot read usage themselves. On a
|
|
6
|
+
* worker whose cache is empty or stale, the same tick pulls those rows from the
|
|
7
|
+
* primary headed device. Both drivers gate on the self role, so this service is
|
|
8
8
|
* safe to register everywhere. Single-executor per destination: each daemon only
|
|
9
9
|
* writes the DESTINATION's own cache, and the merge is newest-wins + idempotent.
|
|
10
10
|
*/
|
|
@@ -24,13 +24,19 @@ export class UsageSyncService extends BasePeriodicService {
|
|
|
24
24
|
// Nothing to release — the supervisor's timer teardown is the only cleanup.
|
|
25
25
|
}
|
|
26
26
|
async onTick(ctx) {
|
|
27
|
-
const { syncFleetUsageSnapshots } = await import('../accounting/usage-sync.js');
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
ctx.log('INFO', `usage-sync: pushed usage to ${
|
|
27
|
+
const { pullUsageFromPrimary, syncFleetUsageSnapshots } = await import('../accounting/usage-sync.js');
|
|
28
|
+
const pushResult = syncFleetUsageSnapshots();
|
|
29
|
+
if (pushResult.pushed.length > 0) {
|
|
30
|
+
ctx.log('INFO', `usage-sync: pushed usage to ${pushResult.pushed.join(', ')}`);
|
|
31
31
|
}
|
|
32
|
-
for (const err of
|
|
32
|
+
for (const err of pushResult.errors) {
|
|
33
33
|
ctx.log('WARN', `usage-sync: ${err.device}: ${err.message}`);
|
|
34
34
|
}
|
|
35
|
+
const pullResult = pullUsageFromPrimary();
|
|
36
|
+
if (pullResult.pulledFrom) {
|
|
37
|
+
ctx.log('INFO', `usage-sync: pulled usage from ${pullResult.pulledFrom}; merged ${pullResult.merged} row(s)`);
|
|
38
|
+
}
|
|
39
|
+
if (pullResult.error)
|
|
40
|
+
ctx.log('WARN', `usage-sync: pull: ${pullResult.error}`);
|
|
35
41
|
}
|
|
36
42
|
}
|
|
@@ -100,7 +100,7 @@ export const DAEMON_SERVICES = [
|
|
|
100
100
|
{
|
|
101
101
|
id: 'usage-sync',
|
|
102
102
|
title: 'Usage snapshot sync',
|
|
103
|
-
description: '
|
|
103
|
+
description: 'Bidirectional per-account usage sync: a headed personal/desktop box pushes its snapshot to worker peers, and a worker with a stale cache pulls from the primary — so workers that cannot read the endpoint themselves still route by real capacity.',
|
|
104
104
|
},
|
|
105
105
|
];
|
|
106
106
|
/** Stable order of service IDs. */
|
|
@@ -121,3 +121,18 @@ export declare function runSessionIndexWarmTick(): Promise<{
|
|
|
121
121
|
indexed: number;
|
|
122
122
|
claimed: boolean;
|
|
123
123
|
}>;
|
|
124
|
+
/**
|
|
125
|
+
* Deferred tool-index pass for large-transcript harnesses (PHNX-3411).
|
|
126
|
+
*
|
|
127
|
+
* Kimi (wire.jsonl) and Grok (chat_history.jsonl) scanners produce only
|
|
128
|
+
* metadata — no events. Calling parseSession for those on the warm tick wedges
|
|
129
|
+
* the Node event loop when the transcript is large and active (observed: several
|
|
130
|
+
* seconds per tick on zion, causing browser IPC ECONNREFUSED). This pass fills
|
|
131
|
+
* the gap: it queries recently-active kimi/grok sessions and calls
|
|
132
|
+
* ensureToolIndex, which uses tool_scan_ledger stamps to skip already-current
|
|
133
|
+
* sessions and applies byte/file budget caps so no large transcript monopolises
|
|
134
|
+
* the tick.
|
|
135
|
+
*/
|
|
136
|
+
export declare function runDeferredToolIndex(): Promise<{
|
|
137
|
+
indexed: number;
|
|
138
|
+
}>;
|
package/dist/lib/daemon-ticks.js
CHANGED
|
@@ -185,3 +185,29 @@ export async function runSessionIndexWarmTick() {
|
|
|
185
185
|
return { indexed: 0, claimed: false };
|
|
186
186
|
return { indexed: scanned, claimed: true };
|
|
187
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Deferred tool-index pass for large-transcript harnesses (PHNX-3411).
|
|
190
|
+
*
|
|
191
|
+
* Kimi (wire.jsonl) and Grok (chat_history.jsonl) scanners produce only
|
|
192
|
+
* metadata — no events. Calling parseSession for those on the warm tick wedges
|
|
193
|
+
* the Node event loop when the transcript is large and active (observed: several
|
|
194
|
+
* seconds per tick on zion, causing browser IPC ECONNREFUSED). This pass fills
|
|
195
|
+
* the gap: it queries recently-active kimi/grok sessions and calls
|
|
196
|
+
* ensureToolIndex, which uses tool_scan_ledger stamps to skip already-current
|
|
197
|
+
* sessions and applies byte/file budget caps so no large transcript monopolises
|
|
198
|
+
* the tick.
|
|
199
|
+
*/
|
|
200
|
+
export async function runDeferredToolIndex() {
|
|
201
|
+
const { querySessionsForDeferredToolIndex } = await import('./session/db.js');
|
|
202
|
+
const { ensureToolIndex } = await import('./session/tool-index.js');
|
|
203
|
+
// Feed the 200 most-recently-active kimi/grok sessions; ensureToolIndex skips
|
|
204
|
+
// any whose tool_scan_ledger stamp is current.
|
|
205
|
+
const sessions = querySessionsForDeferredToolIndex(200);
|
|
206
|
+
if (sessions.length === 0)
|
|
207
|
+
return { indexed: 0 };
|
|
208
|
+
const coverage = await ensureToolIndex(sessions, {
|
|
209
|
+
maxFiles: 20,
|
|
210
|
+
maxBytes: 20 * 1024 * 1024, // 20 MB — bounds one tick even on large transcripts
|
|
211
|
+
});
|
|
212
|
+
return { indexed: coverage.indexedFiles };
|
|
213
|
+
}
|
|
@@ -194,7 +194,11 @@ export declare function devicesPinningBrowserProfile(profile: string): Array<{
|
|
|
194
194
|
export declare function listConfiguredDeviceRoles(roster?: string[]): Record<string, ConfiguredDeviceRole>;
|
|
195
195
|
/** The configured automatic-placement pool mode. Unset means `workers`. */
|
|
196
196
|
export declare function autoPoolMode(): AutoPoolMode;
|
|
197
|
-
/**
|
|
197
|
+
/**
|
|
198
|
+
* A device's auto-launch flags, read by the automatic-placement pool
|
|
199
|
+
* (`filterAutoPool` drops `enabled: false`; `pickBestDevice` boosts
|
|
200
|
+
* `preferred: true` — see `lib/devices/pool.ts`) and by the menu-bar snapshot.
|
|
201
|
+
*/
|
|
198
202
|
export interface AutoLaunchPreference {
|
|
199
203
|
enabled?: boolean;
|
|
200
204
|
preferred?: boolean;
|
|
@@ -294,7 +294,7 @@ export const CONFIG_KEYS = [
|
|
|
294
294
|
visibility: 'shared',
|
|
295
295
|
type: 'bool',
|
|
296
296
|
defaultValue: true,
|
|
297
|
-
description: 'Whether AGI EXT
|
|
297
|
+
description: 'Whether `--device auto` (run, teams, AGI EXT) may pick this device (default on). Off drops it from every automatic-placement path.',
|
|
298
298
|
},
|
|
299
299
|
{
|
|
300
300
|
name: 'auto-launch.preferred',
|
|
@@ -303,7 +303,7 @@ export const CONFIG_KEYS = [
|
|
|
303
303
|
visibility: 'shared',
|
|
304
304
|
type: 'bool',
|
|
305
305
|
defaultValue: false,
|
|
306
|
-
description: 'Boost this device in
|
|
306
|
+
description: 'Boost this device in `--device auto` ranking (default off) — picked ahead of load-equal peers when eligible.',
|
|
307
307
|
},
|
|
308
308
|
];
|
|
309
309
|
/** Look up a key spec by CLI dotted name, or throw listing the known keys. */
|
|
@@ -47,16 +47,25 @@ export declare function wrapRemoteCommand(device: DeviceProfile, cmd: string[]):
|
|
|
47
47
|
*/
|
|
48
48
|
export declare function isAgentsBrowserDrive(cmd: string[]): boolean;
|
|
49
49
|
/**
|
|
50
|
-
* Prefix a remote command
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
50
|
+
* Prefix a remote command with the provenance the far-side browser needs: the
|
|
51
|
+
* `AGENTS_FLEET_REMOTE=1` consent marker AND the caller's resolved actor
|
|
52
|
+
* (`AGENTS_ACTOR*`/`GIT_*`). Without the actor, a task created over
|
|
53
|
+
* `agents ssh <host> 'agents browser …'` is stamped `UNRESOLVED@<host>` because
|
|
54
|
+
* the peer re-resolves identity from its own (empty) env — the same RUSH-2028
|
|
55
|
+
* gap the `--device` dispatch already closes via `withActorEnv`, applied here to
|
|
56
|
+
* the `agents ssh` browser-drive seam so ownership is honest across BOTH paths.
|
|
55
57
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
+
* `wrapRemoteCommand` joins the argv with spaces (POSIX) or base64-encodes it for
|
|
59
|
+
* PowerShell, so a shell-appropriate prelude rides through both: `env VAR=v …` on
|
|
60
|
+
* POSIX, `$env:VAR='v'; …` on PowerShell. The marker stays the FIRST token so the
|
|
61
|
+
* consent gate and the idempotency guard below both still key on it.
|
|
62
|
+
*
|
|
63
|
+
* `provenanceEnv` is injectable so tests can pin a deterministic actor; it
|
|
64
|
+
* defaults to the live resolved actor. Already-marked argv (the `--device`
|
|
65
|
+
* fan-out stamps the marker before {@link buildSshInvocation}) is left unchanged
|
|
66
|
+
* so nothing is doubled.
|
|
58
67
|
*/
|
|
59
|
-
export declare function markFleetRemote(cmd: string[], device: Pick<DeviceProfile, 'shell'>): string[];
|
|
68
|
+
export declare function markFleetRemote(cmd: string[], device: Pick<DeviceProfile, 'shell'>, provenanceEnv?: Record<string, string>): string[];
|
|
60
69
|
/**
|
|
61
70
|
* Build the remote command that starts an INTERACTIVE LOGIN shell inside a
|
|
62
71
|
* mirrored project directory, falling back to the remote home when that
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
18
|
import { assertValidSshTarget, shellQuote } from '../ssh-exec.js';
|
|
19
|
+
import { resolveActor, actorEnv } from '../actor.js';
|
|
19
20
|
import { getCliLaunch } from '../cli-entry.js';
|
|
20
21
|
import { encodePwshBase64 } from '../pwsh.js';
|
|
21
22
|
import { homeRemainder, remoteCdPrefix } from '../project-root.js';
|
|
@@ -104,24 +105,40 @@ export function isAgentsBrowserDrive(cmd) {
|
|
|
104
105
|
return (bin === 'agents' || bin === 'ag') && tokens[1] === 'browser';
|
|
105
106
|
}
|
|
106
107
|
/**
|
|
107
|
-
* Prefix a remote command
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
108
|
+
* Prefix a remote command with the provenance the far-side browser needs: the
|
|
109
|
+
* `AGENTS_FLEET_REMOTE=1` consent marker AND the caller's resolved actor
|
|
110
|
+
* (`AGENTS_ACTOR*`/`GIT_*`). Without the actor, a task created over
|
|
111
|
+
* `agents ssh <host> 'agents browser …'` is stamped `UNRESOLVED@<host>` because
|
|
112
|
+
* the peer re-resolves identity from its own (empty) env — the same RUSH-2028
|
|
113
|
+
* gap the `--device` dispatch already closes via `withActorEnv`, applied here to
|
|
114
|
+
* the `agents ssh` browser-drive seam so ownership is honest across BOTH paths.
|
|
112
115
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
116
|
+
* `wrapRemoteCommand` joins the argv with spaces (POSIX) or base64-encodes it for
|
|
117
|
+
* PowerShell, so a shell-appropriate prelude rides through both: `env VAR=v …` on
|
|
118
|
+
* POSIX, `$env:VAR='v'; …` on PowerShell. The marker stays the FIRST token so the
|
|
119
|
+
* consent gate and the idempotency guard below both still key on it.
|
|
120
|
+
*
|
|
121
|
+
* `provenanceEnv` is injectable so tests can pin a deterministic actor; it
|
|
122
|
+
* defaults to the live resolved actor. Already-marked argv (the `--device`
|
|
123
|
+
* fan-out stamps the marker before {@link buildSshInvocation}) is left unchanged
|
|
124
|
+
* so nothing is doubled.
|
|
115
125
|
*/
|
|
116
|
-
export function markFleetRemote(cmd, device) {
|
|
126
|
+
export function markFleetRemote(cmd, device, provenanceEnv = actorEnv(resolveActor())) {
|
|
117
127
|
if (device.shell === 'powershell') {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
// Exact-match guard, symmetric with the POSIX branch below: the marker token
|
|
129
|
+
// is always this literal, so `startsWith` would only loosen it for no gain.
|
|
130
|
+
if (cmd[0] === `$env:AGENTS_FLEET_REMOTE='1';`)
|
|
131
|
+
return cmd;
|
|
132
|
+
const prelude = [
|
|
133
|
+
`$env:AGENTS_FLEET_REMOTE='1';`,
|
|
134
|
+
...Object.entries(provenanceEnv).map(([k, v]) => `$env:${k}='${v.replace(/'/g, "''")}';`),
|
|
135
|
+
];
|
|
136
|
+
return [...prelude, ...cmd];
|
|
121
137
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
if (cmd[0] === 'env' && cmd[1] === 'AGENTS_FLEET_REMOTE=1')
|
|
139
|
+
return cmd;
|
|
140
|
+
const actorTokens = Object.entries(provenanceEnv).map(([k, v]) => shellQuote(`${k}=${v}`));
|
|
141
|
+
return ['env', 'AGENTS_FLEET_REMOTE=1', ...actorTokens, ...cmd];
|
|
125
142
|
}
|
|
126
143
|
/**
|
|
127
144
|
* Build the remote command that starts an INTERACTIVE LOGIN shell inside a
|
|
@@ -145,8 +145,12 @@ export function parseProbeOutput(host, stdout, fetchedAt) {
|
|
|
145
145
|
* numbers, mirroring how garbage POSIX output degrades. */
|
|
146
146
|
export function parseWinProbeOutput(host, stdout, fetchedAt) {
|
|
147
147
|
const m = stdout.match(/AGWINSTAT load=([0-9.]*) freeKb=([0-9]+) totalKb=([0-9]+) ncpu=([0-9]+)(?: diskFreeKb=([0-9.]+) diskTotalKb=([0-9.]+))?/);
|
|
148
|
+
// Unparseable output still means the probe RAN — the box answered, we just
|
|
149
|
+
// could not read it. Stamp specsFetchedAt anyway so a hardware-fact carry
|
|
150
|
+
// forward (retainHardwareFacts, RUSH-3096) has a real observation moment
|
|
151
|
+
// instead of undefined.
|
|
148
152
|
if (!m)
|
|
149
|
-
return { host, reachable: true, fetchedAt };
|
|
153
|
+
return { host, reachable: true, fetchedAt, specsFetchedAt: fetchedAt };
|
|
150
154
|
const loadPercent = m[1] === '' ? undefined : parseFloat(m[1]);
|
|
151
155
|
const freeKb = parseInt(m[2], 10);
|
|
152
156
|
const totalKb = parseInt(m[3], 10);
|
|
@@ -16,12 +16,20 @@
|
|
|
16
16
|
* | no device marked | every online device (unchanged behavior) |
|
|
17
17
|
* | some marked `worker` | ONLY those workers |
|
|
18
18
|
* | marked `personal` / `desktop` | never, under either state |
|
|
19
|
+
* | `auto-launch.enabled` off | never, whatever its role or the mode |
|
|
19
20
|
*
|
|
20
|
-
* `auto.pool all` turns the allowlist off; `personal` and `desktop` stay
|
|
21
|
+
* `auto.pool all` turns the WORKER allowlist off; `personal` and `desktop` stay
|
|
21
22
|
* excluded, because a box the user sits at (personal) or a headed always-on
|
|
22
23
|
* release/credential box (desktop) is marked precisely so agents stay off it.
|
|
24
|
+
*
|
|
25
|
+
* A device the operator turned off with `agents devices disable <name>`
|
|
26
|
+
* (`auto-launch.enabled` = false) is dropped here too — one operator switch,
|
|
27
|
+
* one place, so `disable` removes a box from EVERY automatic-placement path
|
|
28
|
+
* (run/teams/ssh auto), not just one surface. Its sibling
|
|
29
|
+
* `auto-launch.preferred` does not narrow the pool; it BOOSTS a member in the
|
|
30
|
+
* ranker ({@link autoLaunchPreferredSet}, applied by `pickBestDevice`).
|
|
23
31
|
*/
|
|
24
|
-
import { type AutoPoolMode, type ConfiguredDeviceRole } from '../device-config.js';
|
|
32
|
+
import { type AutoLaunchPreference, type AutoPoolMode, type ConfiguredDeviceRole } from '../device-config.js';
|
|
25
33
|
export interface AutoPoolOptions {
|
|
26
34
|
/** Pool mode; defaults to the configured `auto.pool`. */
|
|
27
35
|
mode?: AutoPoolMode;
|
|
@@ -34,6 +42,14 @@ export interface AutoPoolOptions {
|
|
|
34
42
|
* {@link listConfiguredDeviceRoles}.
|
|
35
43
|
*/
|
|
36
44
|
roster?: string[];
|
|
45
|
+
/**
|
|
46
|
+
* Auto-launch flags by device name; defaults to the fleet-shared block for
|
|
47
|
+
* the roster (or the pool). A device whose `enabled` is `false` is dropped
|
|
48
|
+
* from the pool. Inject `{}` in a pure unit test to keep the rule off disk,
|
|
49
|
+
* exactly as `roles: {}` does for the role rule. See
|
|
50
|
+
* {@link loadAutoLaunchPreferences}.
|
|
51
|
+
*/
|
|
52
|
+
autoLaunch?: Record<string, AutoLaunchPreference>;
|
|
37
53
|
}
|
|
38
54
|
/**
|
|
39
55
|
* Narrow a candidate host list to the devices automatic placement may pick.
|
|
@@ -44,6 +60,13 @@ export interface AutoPoolOptions {
|
|
|
44
60
|
* quietly widening back to the full fleet.
|
|
45
61
|
*/
|
|
46
62
|
export declare function filterAutoPool(pool: string[], opts?: AutoPoolOptions): string[];
|
|
63
|
+
/**
|
|
64
|
+
* Normalized hosts the operator boosted with `auto-launch.preferred` = true —
|
|
65
|
+
* the set `pickBestDevice` ranks ahead of its peers. Unlike the disable drop,
|
|
66
|
+
* a preference never removes a device: an eligible non-preferred box is still
|
|
67
|
+
* picked when no preferred one is available.
|
|
68
|
+
*/
|
|
69
|
+
export declare function autoLaunchPreferredSet(pool: string[], opts?: AutoPoolOptions): Set<string>;
|
|
47
70
|
/** True when this host is one automatic placement may pick. */
|
|
48
71
|
export declare function isAutoPoolMember(host: string, opts?: AutoPoolOptions): boolean;
|
|
49
72
|
/** Device names explicitly marked `worker`, in registry order. */
|
package/dist/lib/devices/pool.js
CHANGED
|
@@ -16,12 +16,20 @@
|
|
|
16
16
|
* | no device marked | every online device (unchanged behavior) |
|
|
17
17
|
* | some marked `worker` | ONLY those workers |
|
|
18
18
|
* | marked `personal` / `desktop` | never, under either state |
|
|
19
|
+
* | `auto-launch.enabled` off | never, whatever its role or the mode |
|
|
19
20
|
*
|
|
20
|
-
* `auto.pool all` turns the allowlist off; `personal` and `desktop` stay
|
|
21
|
+
* `auto.pool all` turns the WORKER allowlist off; `personal` and `desktop` stay
|
|
21
22
|
* excluded, because a box the user sits at (personal) or a headed always-on
|
|
22
23
|
* release/credential box (desktop) is marked precisely so agents stay off it.
|
|
24
|
+
*
|
|
25
|
+
* A device the operator turned off with `agents devices disable <name>`
|
|
26
|
+
* (`auto-launch.enabled` = false) is dropped here too — one operator switch,
|
|
27
|
+
* one place, so `disable` removes a box from EVERY automatic-placement path
|
|
28
|
+
* (run/teams/ssh auto), not just one surface. Its sibling
|
|
29
|
+
* `auto-launch.preferred` does not narrow the pool; it BOOSTS a member in the
|
|
30
|
+
* ranker ({@link autoLaunchPreferredSet}, applied by `pickBestDevice`).
|
|
23
31
|
*/
|
|
24
|
-
import { autoPoolMode, listConfiguredDeviceRoles } from '../device-config.js';
|
|
32
|
+
import { autoPoolMode, listConfiguredDeviceRoles, loadAutoLaunchPreferences, } from '../device-config.js';
|
|
25
33
|
import { normalizeHost } from '../machine-id.js';
|
|
26
34
|
/**
|
|
27
35
|
* Roles that automatic placement never picks, whatever the pool mode.
|
|
@@ -44,7 +52,10 @@ export function filterAutoPool(pool, opts = {}) {
|
|
|
44
52
|
const roles = opts.roles ?? listConfiguredDeviceRoles(opts.roster ?? pool);
|
|
45
53
|
const byHost = new Map(Object.entries(roles).map(([name, role]) => [normalizeHost(name), role]));
|
|
46
54
|
const roleOf = (host) => byHost.get(normalizeHost(host));
|
|
55
|
+
const disabled = disabledAutoLaunchSet(pool, opts);
|
|
47
56
|
const eligible = pool.filter((host) => {
|
|
57
|
+
if (disabled.has(normalizeHost(host)))
|
|
58
|
+
return false;
|
|
48
59
|
const role = roleOf(host);
|
|
49
60
|
return role === undefined || !NEVER_AUTO.has(role);
|
|
50
61
|
});
|
|
@@ -56,6 +67,25 @@ export function filterAutoPool(pool, opts = {}) {
|
|
|
56
67
|
return eligible;
|
|
57
68
|
return eligible.filter((host) => roleOf(host) === 'worker');
|
|
58
69
|
}
|
|
70
|
+
/** Normalized hosts the operator turned off with `auto-launch.enabled` = false. */
|
|
71
|
+
function disabledAutoLaunchSet(pool, opts) {
|
|
72
|
+
const prefs = opts.autoLaunch ?? loadAutoLaunchPreferences(opts.roster ?? pool);
|
|
73
|
+
return new Set(Object.entries(prefs)
|
|
74
|
+
.filter(([, pref]) => pref.enabled === false)
|
|
75
|
+
.map(([name]) => normalizeHost(name)));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Normalized hosts the operator boosted with `auto-launch.preferred` = true —
|
|
79
|
+
* the set `pickBestDevice` ranks ahead of its peers. Unlike the disable drop,
|
|
80
|
+
* a preference never removes a device: an eligible non-preferred box is still
|
|
81
|
+
* picked when no preferred one is available.
|
|
82
|
+
*/
|
|
83
|
+
export function autoLaunchPreferredSet(pool, opts = {}) {
|
|
84
|
+
const prefs = opts.autoLaunch ?? loadAutoLaunchPreferences(opts.roster ?? pool);
|
|
85
|
+
return new Set(Object.entries(prefs)
|
|
86
|
+
.filter(([, pref]) => pref.preferred === true)
|
|
87
|
+
.map(([name]) => normalizeHost(name)));
|
|
88
|
+
}
|
|
59
89
|
/** True when this host is one automatic placement may pick. */
|
|
60
90
|
export function isAutoPoolMember(host, opts = {}) {
|
|
61
91
|
return filterAutoPool([host], opts).length > 0;
|