@phnx-labs/agents-cli 1.22.6 → 1.22.8
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 +131 -1
- package/README.md +7 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +61 -0
- package/dist/commands/exec.js +83 -16
- package/dist/commands/feed.d.ts +2 -1
- package/dist/commands/feed.js +47 -20
- package/dist/commands/focus.js +22 -1
- package/dist/commands/harness.d.ts +0 -1
- package/dist/commands/harness.js +60 -4
- package/dist/commands/models.js +2 -2
- package/dist/commands/monitors.js +2 -2
- package/dist/commands/projects.js +122 -107
- package/dist/commands/routines.js +2 -2
- package/dist/commands/run-account-picker.js +2 -0
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/sessions-backfill.d.ts +33 -0
- package/dist/commands/sessions-backfill.js +83 -1
- package/dist/commands/sessions-stats.d.ts +36 -0
- package/dist/commands/sessions-stats.js +263 -0
- package/dist/commands/sessions.d.ts +1 -1
- package/dist/commands/sessions.js +21 -1
- package/dist/commands/snapshot.d.ts +11 -0
- package/dist/commands/snapshot.js +107 -0
- package/dist/commands/teams.js +2 -1
- package/dist/commands/view.d.ts +9 -1
- package/dist/commands/view.js +63 -12
- package/dist/index.js +4 -2
- package/dist/lib/activity.js +2 -2
- package/dist/lib/agents.js +68 -11
- package/dist/lib/analytics/recipes.js +11 -5
- package/dist/lib/browser/ipc.js +2 -0
- package/dist/lib/browser/profiles.d.ts +15 -7
- package/dist/lib/browser/profiles.js +53 -12
- package/dist/lib/browser/remote-control.d.ts +35 -0
- package/dist/lib/browser/remote-control.js +48 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +19 -1
- package/dist/lib/browser/types.d.ts +14 -2
- package/dist/lib/byok-usage.d.ts +38 -0
- package/dist/lib/byok-usage.js +117 -0
- package/dist/lib/capabilities.js +1 -1
- package/dist/lib/device-config.js +8 -0
- package/dist/lib/exec.d.ts +20 -0
- package/dist/lib/exec.js +73 -8
- package/dist/lib/feed-outcome.d.ts +1 -0
- package/dist/lib/feed-outcome.js +2 -0
- package/dist/lib/feed-post.js +1 -1
- package/dist/lib/feed-ranking.d.ts +1 -0
- package/dist/lib/feed-ranking.js +4 -0
- package/dist/lib/feed.d.ts +4 -1
- package/dist/lib/feed.js +25 -0
- package/dist/lib/hosts/passthrough.d.ts +10 -1
- package/dist/lib/hosts/passthrough.js +23 -3
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/mcp.js +6 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tiers.js +4 -1
- package/dist/lib/models.js +63 -0
- package/dist/lib/placement.d.ts +82 -0
- package/dist/lib/placement.js +188 -0
- package/dist/lib/profiles.d.ts +73 -10
- package/dist/lib/profiles.js +133 -15
- package/dist/lib/project-focus.d.ts +9 -0
- package/dist/lib/project-focus.js +23 -0
- package/dist/lib/project-key.d.ts +1 -1
- package/dist/lib/project-key.js +1 -1
- package/dist/lib/project-probe.d.ts +18 -0
- package/dist/lib/project-probe.js +46 -0
- package/dist/lib/project-status.d.ts +56 -0
- package/dist/lib/project-status.js +125 -18
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/rotate.d.ts +19 -4
- package/dist/lib/rotate.js +24 -1
- package/dist/lib/routines.d.ts +2 -0
- package/dist/lib/runner.d.ts +3 -0
- package/dist/lib/runner.js +90 -7
- 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/audit.js +1 -1
- package/dist/lib/secrets/index.d.ts +1 -0
- package/dist/lib/secrets/index.js +29 -15
- package/dist/lib/secrets/remote.d.ts +1 -1
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/session/active.d.ts +2 -0
- package/dist/lib/session/bash-command.d.ts +2 -3
- package/dist/lib/session/db.d.ts +92 -1
- package/dist/lib/session/db.js +230 -1
- package/dist/lib/session/digest.d.ts +1 -1
- package/dist/lib/session/digest.js +1 -1
- package/dist/lib/share/publish.js +24 -0
- package/dist/lib/snapshot.d.ts +103 -0
- package/dist/lib/snapshot.js +99 -0
- package/dist/lib/startup/command-registry.d.ts +1 -1
- package/dist/lib/startup/command-registry.js +2 -2
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/types.d.ts +11 -2
- package/dist/lib/usage.d.ts +5 -0
- package/dist/lib/usage.js +3 -3
- package/package.json +1 -1
- package/dist/commands/activity.d.ts +0 -87
- package/dist/commands/activity.js +0 -346
package/dist/lib/rotate.js
CHANGED
|
@@ -13,6 +13,8 @@ import { getProjectRunConfigs } from './run-config.js';
|
|
|
13
13
|
import { emit } from './events.js';
|
|
14
14
|
import { getUsageInfoByIdentity, getUsageLookupKey, deriveUsageStatusFromSnapshot, } from './usage.js';
|
|
15
15
|
import { readAccountHeadroom } from './fleet-cache.js';
|
|
16
|
+
import { machineId } from './machine-id.js';
|
|
17
|
+
import { readAuthHealthCache, authCacheKey, isDeadVerdict } from './auth-health.js';
|
|
16
18
|
function getRotateDir() {
|
|
17
19
|
const dir = path.join(getHelpersDir(), 'rotate');
|
|
18
20
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -65,8 +67,14 @@ export function setGlobalRunStrategy(agent, strategy) {
|
|
|
65
67
|
meta.run[agent] = { ...(meta.run[agent] ?? {}), strategy };
|
|
66
68
|
writeMeta(meta);
|
|
67
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether an account may be rotated INTO right now. Defined in terms of
|
|
72
|
+
* {@link readinessFromCandidate} so the router's pick gate and the pre-flight
|
|
73
|
+
* warning can never disagree: an account is eligible iff its readiness is
|
|
74
|
+
* `ready` — signed in, not server-revoked, and not out of usage.
|
|
75
|
+
*/
|
|
68
76
|
function isRotationEligible(candidate) {
|
|
69
|
-
return
|
|
77
|
+
return readinessFromCandidate(candidate).ready;
|
|
70
78
|
}
|
|
71
79
|
/**
|
|
72
80
|
* Whether a version home can actually authenticate a launch.
|
|
@@ -146,6 +154,13 @@ export function readinessFromCandidate(candidate) {
|
|
|
146
154
|
if (!candidate.signedIn) {
|
|
147
155
|
return { ready: false, reason: 'signed_out', email: candidate.email };
|
|
148
156
|
}
|
|
157
|
+
// A token the daemon's live probe saw rejected (401/403 -> `revoked`) will fail
|
|
158
|
+
// auth at spawn no matter how much usage headroom it has. Exclude it BEFORE the
|
|
159
|
+
// usage gate so rotation never routes into a doomed login. Fail-open: any other
|
|
160
|
+
// (or null) verdict does not gate — see `RotateCandidate.authVerdict`.
|
|
161
|
+
if (candidate.authVerdict !== null && isDeadVerdict(candidate.authVerdict)) {
|
|
162
|
+
return { ready: false, reason: 'revoked', email: candidate.email };
|
|
163
|
+
}
|
|
149
164
|
if (hasUsageAvailable(candidate)) {
|
|
150
165
|
return { ready: true };
|
|
151
166
|
}
|
|
@@ -495,6 +510,12 @@ export function formatNoHealthyHarnessError(summaries, nowMs = Date.now()) {
|
|
|
495
510
|
}
|
|
496
511
|
export async function collectRunCandidates(agent) {
|
|
497
512
|
const versions = listInstalledVersions(agent);
|
|
513
|
+
// Read the local auth-health probe cache once (cache-only, no network — the
|
|
514
|
+
// daemon is the sole writer). A `revoked` verdict for a (host, agent, version)
|
|
515
|
+
// excludes that account from the pick; a missing row is fail-open. Keyed by the
|
|
516
|
+
// LOCAL host — routing decides which local version to launch.
|
|
517
|
+
const authCache = readAuthHealthCache();
|
|
518
|
+
const localHost = machineId();
|
|
498
519
|
const rows = await Promise.all(versions.map(async (version) => {
|
|
499
520
|
const home = getVersionHomePath(agent, version);
|
|
500
521
|
const info = await getAccountInfo(agent, home);
|
|
@@ -510,6 +531,7 @@ export async function collectRunCandidates(agent) {
|
|
|
510
531
|
// lives — see isLaunchableSignedIn. Do not reuse the active-home fallback
|
|
511
532
|
// identity for routing, or empty version homes look healthy and die at spawn.
|
|
512
533
|
const launchable = isLaunchableSignedIn(info.signedIn, credentialPresence(agent, home));
|
|
534
|
+
const authVerdict = authCache[authCacheKey(localHost, agent, version)]?.verdict ?? null;
|
|
513
535
|
return {
|
|
514
536
|
agent,
|
|
515
537
|
version,
|
|
@@ -521,6 +543,7 @@ export async function collectRunCandidates(agent) {
|
|
|
521
543
|
usageStatus: launchable ? info.usageStatus : null,
|
|
522
544
|
plan: launchable ? info.plan : null,
|
|
523
545
|
signedIn: launchable,
|
|
546
|
+
authVerdict,
|
|
524
547
|
lastActive: info.lastActive,
|
|
525
548
|
};
|
|
526
549
|
}));
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -239,6 +239,8 @@ export interface RunMeta {
|
|
|
239
239
|
pid: number | null;
|
|
240
240
|
/** Process birth time (epoch ms) recorded at spawn for pid-reuse detection. */
|
|
241
241
|
spawnedAt?: number;
|
|
242
|
+
/** Configured execution deadline persisted for daemon-restart recovery. */
|
|
243
|
+
timeoutMs?: number;
|
|
242
244
|
/**
|
|
243
245
|
* `missed` is not an execution outcome — it is the record that a scheduled
|
|
244
246
|
* fire never happened (the daemon was down, asleep, or wedged when it came
|
package/dist/lib/runner.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ export interface RunResult {
|
|
|
23
23
|
meta: RunMeta;
|
|
24
24
|
reportPath: string | null;
|
|
25
25
|
}
|
|
26
|
+
export declare class RoutineAlreadyRunningError extends Error {
|
|
27
|
+
constructor(jobName: string, runId: string);
|
|
28
|
+
}
|
|
26
29
|
/** Agents the daemon can actually run, derived from the command table above
|
|
27
30
|
* so the `--agent` help and any validation can never drift from it. */
|
|
28
31
|
export declare const ROUTINE_AGENT_IDS: readonly string[];
|
package/dist/lib/runner.js
CHANGED
|
@@ -17,7 +17,7 @@ import { spawn, execFileSync } from 'child_process';
|
|
|
17
17
|
import * as fs from 'fs';
|
|
18
18
|
import * as path from 'path';
|
|
19
19
|
import * as os from 'os';
|
|
20
|
-
import { resolveJobPrompt, parseTimeout, writeRunMeta, getRunDir, checkJobDeviceEligibility, finalizeRunMeta, } from './routines.js';
|
|
20
|
+
import { resolveJobPrompt, parseTimeout, writeRunMeta, listRuns, getJobRunsDir, getRunDir, checkJobDeviceEligibility, finalizeRunMeta, } from './routines.js';
|
|
21
21
|
import { getRunsDir } from './state.js';
|
|
22
22
|
import { prepareJobHome, buildSpawnEnv, getJobHomePath } from './sandbox.js';
|
|
23
23
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
@@ -26,7 +26,9 @@ import { normalizeMode, resolveHeadlessMode, buildExecEnv, detectRateLimit, dete
|
|
|
26
26
|
import { resolveActor } from './actor.js';
|
|
27
27
|
import { loadTask as loadHostTask } from './hosts/tasks.js';
|
|
28
28
|
import { reconcileTask as reconcileHostTask } from './hosts/reconcile.js';
|
|
29
|
-
import { backgroundSpawnOptions } from './platform/process.js';
|
|
29
|
+
import { backgroundSpawnOptions, killTree } from './platform/process.js';
|
|
30
|
+
import lockfile from 'proper-lockfile';
|
|
31
|
+
import { ensureLockTarget } from './fs-atomic.js';
|
|
30
32
|
import { walkForFiles } from './fs-walk.js';
|
|
31
33
|
import { getBinaryPath, isVersionInstalled, resolveVersion } from './versions.js';
|
|
32
34
|
import { getConfiguredRunStrategy, resolveRunVersion, resolveAccountVersion, rotationFailoverChain, readinessFromCandidate, formatNoHealthyAccountError, } from './rotate.js';
|
|
@@ -34,6 +36,66 @@ import { readAuthHealth, isDeadVerdict } from './auth-health.js';
|
|
|
34
36
|
import { machineId } from './machine-id.js';
|
|
35
37
|
import { isSelfUpdatingAgent } from './agents.js';
|
|
36
38
|
import { expandLocalHome, getProjectRoot } from './project-root.js';
|
|
39
|
+
export class RoutineAlreadyRunningError extends Error {
|
|
40
|
+
constructor(jobName, runId) {
|
|
41
|
+
super(`Routine '${jobName}' already has a running execution (${runId})`);
|
|
42
|
+
this.name = 'RoutineAlreadyRunningError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const ROUTINE_LAUNCH_LOCK_STALE_MS = 30_000;
|
|
46
|
+
const ROUTINE_LAUNCH_LOCK_WAIT_MS = 10_000;
|
|
47
|
+
function activeRoutineRun(config) {
|
|
48
|
+
const timeoutMs = parseTimeout(config.timeout) || 10 * 60 * 1000;
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const runs = listRuns(config.name);
|
|
51
|
+
for (let i = runs.length - 1; i >= 0; i--) {
|
|
52
|
+
const run = runs[i];
|
|
53
|
+
if (run.status !== 'running')
|
|
54
|
+
continue;
|
|
55
|
+
if (run.pid && isPidOurs(run.pid, run.spawnedAt))
|
|
56
|
+
return run;
|
|
57
|
+
if (!run.pid) {
|
|
58
|
+
const startedAt = Date.parse(run.startedAt);
|
|
59
|
+
if (Number.isFinite(startedAt) && now - startedAt < (run.timeoutMs ?? timeoutMs))
|
|
60
|
+
return run;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
async function withRoutineLaunchClaim(config, launch) {
|
|
66
|
+
const target = path.join(getJobRunsDir(config.name), '.launch-claim');
|
|
67
|
+
ensureLockTarget(target, '', 0o700);
|
|
68
|
+
const release = await lockfile.lock(target, {
|
|
69
|
+
stale: ROUTINE_LAUNCH_LOCK_STALE_MS,
|
|
70
|
+
retries: {
|
|
71
|
+
retries: Math.ceil(ROUTINE_LAUNCH_LOCK_WAIT_MS / 100),
|
|
72
|
+
factor: 1,
|
|
73
|
+
minTimeout: 100,
|
|
74
|
+
maxTimeout: 100,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
const active = activeRoutineRun(config);
|
|
79
|
+
if (active)
|
|
80
|
+
throw new RoutineAlreadyRunningError(config.name, active.runId);
|
|
81
|
+
return await launch();
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await release();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function terminateRoutineTree(pid) {
|
|
88
|
+
if (!pid)
|
|
89
|
+
return;
|
|
90
|
+
if (process.platform === 'win32') {
|
|
91
|
+
killTree(pid);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
process.kill(-pid, 'SIGKILL');
|
|
96
|
+
}
|
|
97
|
+
catch { /* already exited */ }
|
|
98
|
+
}
|
|
37
99
|
/** CLI command templates per agent, with {prompt} as a placeholder. */
|
|
38
100
|
const AGENT_COMMANDS = {
|
|
39
101
|
claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
|
|
@@ -1086,6 +1148,9 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1086
1148
|
process.stderr.write(`[agents] daemon: skipping '${config.name}' — ${eligibility.message}\n`);
|
|
1087
1149
|
throw new Error(eligibility.message);
|
|
1088
1150
|
}
|
|
1151
|
+
return withRoutineLaunchClaim(config, () => executeJobDetachedClaimed(config, hooks));
|
|
1152
|
+
}
|
|
1153
|
+
async function executeJobDetachedClaimed(config, hooks) {
|
|
1089
1154
|
// Placement (hostStrategy / bare host:) — dispatch off-box and return; the
|
|
1090
1155
|
// monitor finalizes host: runs, cloud runs stay terminal when dispatch ends.
|
|
1091
1156
|
// Either way the in-process onFinish hook does not fire for off-box routines
|
|
@@ -1161,6 +1226,7 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1161
1226
|
...(config.workflow ? { workflow: config.workflow } : {}),
|
|
1162
1227
|
pid: null,
|
|
1163
1228
|
spawnedAt: Date.now(),
|
|
1229
|
+
timeoutMs: parseTimeout(config.timeout) || 10 * 60 * 1000,
|
|
1164
1230
|
status: 'running',
|
|
1165
1231
|
startedAt: new Date().toISOString(),
|
|
1166
1232
|
completedAt: null,
|
|
@@ -1195,10 +1261,13 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1195
1261
|
env: spawnEnv,
|
|
1196
1262
|
});
|
|
1197
1263
|
let settled = false;
|
|
1264
|
+
let timeoutTimer;
|
|
1198
1265
|
const settle = (status, exitCode, errorMessage) => {
|
|
1199
1266
|
if (settled)
|
|
1200
1267
|
return;
|
|
1201
1268
|
settled = true;
|
|
1269
|
+
if (timeoutTimer)
|
|
1270
|
+
clearTimeout(timeoutTimer);
|
|
1202
1271
|
finalizeRunMeta(meta, status, exitCode, errorMessage ? { errorMessage } : undefined);
|
|
1203
1272
|
writeRunMeta(meta);
|
|
1204
1273
|
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
@@ -1212,6 +1281,10 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1212
1281
|
// can read report.md (RUSH-2030). Best-effort; never breaks finalization.
|
|
1213
1282
|
safeHook(hooks?.onFinish ? () => hooks.onFinish(meta) : undefined);
|
|
1214
1283
|
};
|
|
1284
|
+
timeoutTimer = setTimeout(() => {
|
|
1285
|
+
terminateRoutineTree(child.pid ?? null);
|
|
1286
|
+
settle('timeout', null, 'exceeded configured timeout');
|
|
1287
|
+
}, meta.timeoutMs);
|
|
1215
1288
|
child.on('exit', (code) => {
|
|
1216
1289
|
let logText = '';
|
|
1217
1290
|
try {
|
|
@@ -1249,7 +1322,7 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1249
1322
|
catch { /* fd already closed */ }
|
|
1250
1323
|
meta.pid = child.pid || null;
|
|
1251
1324
|
writeRunMeta(meta);
|
|
1252
|
-
return meta;
|
|
1325
|
+
return { ...meta };
|
|
1253
1326
|
}
|
|
1254
1327
|
/**
|
|
1255
1328
|
* Detached (fire-and-forget) execution for a command-mode routine. Mirrors the
|
|
@@ -1288,6 +1361,7 @@ function executeCommandJobDetached(config, hooks) {
|
|
|
1288
1361
|
command: config.command,
|
|
1289
1362
|
pid: null,
|
|
1290
1363
|
spawnedAt: Date.now(),
|
|
1364
|
+
timeoutMs: parseTimeout(config.timeout) || 10 * 60 * 1000,
|
|
1291
1365
|
status: 'running',
|
|
1292
1366
|
startedAt: new Date().toISOString(),
|
|
1293
1367
|
completedAt: null,
|
|
@@ -1302,17 +1376,24 @@ function executeCommandJobDetached(config, hooks) {
|
|
|
1302
1376
|
// fire-and-forget call, so the exit event fires here. (monitorRunningJobs no
|
|
1303
1377
|
// longer force-fails command jobs; it reads exit-code only on the restart edge.)
|
|
1304
1378
|
let settled = false;
|
|
1379
|
+
let timeoutTimer;
|
|
1305
1380
|
const settle = (status, exitCode, errorMessage) => {
|
|
1306
1381
|
if (settled)
|
|
1307
1382
|
return;
|
|
1308
1383
|
settled = true;
|
|
1384
|
+
if (timeoutTimer)
|
|
1385
|
+
clearTimeout(timeoutTimer);
|
|
1309
1386
|
finalizeRunMeta(meta, status, exitCode, errorMessage ? { errorMessage } : undefined);
|
|
1310
1387
|
writeRunMeta(meta);
|
|
1311
|
-
timer.end({ status, exitCode, runId, ...(errorMessage ? { error: errorMessage } : {}) });
|
|
1388
|
+
timer.end({ status, exitCode: exitCode ?? undefined, runId, ...(errorMessage ? { error: errorMessage } : {}) });
|
|
1312
1389
|
// Finish notification (RUSH-2030). For command routines the threshold only
|
|
1313
1390
|
// surfaces failures, decided in routine-notify.ts. Best-effort.
|
|
1314
1391
|
safeHook(hooks?.onFinish ? () => hooks.onFinish(meta) : undefined);
|
|
1315
1392
|
};
|
|
1393
|
+
timeoutTimer = setTimeout(() => {
|
|
1394
|
+
terminateRoutineTree(child.pid ?? null);
|
|
1395
|
+
settle('timeout', null, 'exceeded configured timeout');
|
|
1396
|
+
}, meta.timeoutMs);
|
|
1316
1397
|
child.on('exit', (code) => settle(code === 0 ? 'completed' : 'failed', code ?? 1));
|
|
1317
1398
|
child.on('error', (err) => {
|
|
1318
1399
|
settle('failed', 1, err.message);
|
|
@@ -1325,7 +1406,7 @@ function executeCommandJobDetached(config, hooks) {
|
|
|
1325
1406
|
catch { /* fd already closed */ }
|
|
1326
1407
|
meta.pid = child.pid || null;
|
|
1327
1408
|
writeRunMeta(meta);
|
|
1328
|
-
return meta;
|
|
1409
|
+
return { ...meta };
|
|
1329
1410
|
}
|
|
1330
1411
|
function extractAndSaveReport(stdoutPath, agentType, runDir) {
|
|
1331
1412
|
try {
|
|
@@ -1508,8 +1589,10 @@ export function monitorRunningJobs() {
|
|
|
1508
1589
|
// parse or extract. Reap them on pid liveness alone.
|
|
1509
1590
|
const isCommandRun = Boolean(meta.command) || !meta.agent;
|
|
1510
1591
|
const wallClockMs = Date.now() - Date.parse(meta.startedAt);
|
|
1511
|
-
|
|
1512
|
-
|
|
1592
|
+
const timeoutMs = meta.timeoutMs ?? MAX_WALL_CLOCK_MS;
|
|
1593
|
+
if (Number.isFinite(wallClockMs) && wallClockMs > timeoutMs) {
|
|
1594
|
+
terminateRoutineTree(meta.pid);
|
|
1595
|
+
finalizeRunMeta(meta, 'timeout', null, { errorMessage: 'exceeded configured timeout' });
|
|
1513
1596
|
writeRunMeta(meta);
|
|
1514
1597
|
if (!isCommandRun) {
|
|
1515
1598
|
extractAndSaveReport(stdoutPath, meta.agent, runDirPath);
|
|
Binary file
|
|
Binary file
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*
|
|
20
20
|
* The event vocabulary, all audit-level and non-milestone (so they surface in
|
|
21
21
|
* `agents events` and the persisted audit trail, but are NOT required in the
|
|
22
|
-
* curated `agents
|
|
22
|
+
* curated `agents feed` surface):
|
|
23
23
|
* - `secrets.get` — a value was READ (exec inject, export, `view --reveal`,
|
|
24
24
|
* raw `get <item>`, remote resolve, sync push,
|
|
25
25
|
* `run --secrets`, and every other bundle read).
|
|
@@ -121,6 +121,7 @@ export declare function withRawKeychainServiceNames<T>(fn: () => T): T;
|
|
|
121
121
|
* the re-key migration and tests; runtime callers go through the primitives,
|
|
122
122
|
* which apply this transparently. */
|
|
123
123
|
export declare function hashedServiceName(item: string, key: Buffer): string;
|
|
124
|
+
export declare function readHmacKeyRecord(): HmacKeyRecord | null;
|
|
124
125
|
/**
|
|
125
126
|
* Heal a `hmackey` item that an OLD helper (pre the metadata/hmackey no-ACL
|
|
126
127
|
* migration fix) re-stamped with a biometry ACL. Such an item makes EVERY hashed
|
|
@@ -235,7 +235,7 @@ function parseHmacKeyRecord(raw) {
|
|
|
235
235
|
}
|
|
236
236
|
return null;
|
|
237
237
|
}
|
|
238
|
-
function readHmacKeyRecord() {
|
|
238
|
+
export function readHmacKeyRecord() {
|
|
239
239
|
// HMAC_KEY_ITEM is exempt from the transform, so this routes to the helper
|
|
240
240
|
// (or the test backend) under its literal name. The item is no-ACL, so the
|
|
241
241
|
// read is silent — attest that to the storm guard so a headless hashed-name
|
|
@@ -247,7 +247,30 @@ function readHmacKeyRecord() {
|
|
|
247
247
|
catch {
|
|
248
248
|
return null;
|
|
249
249
|
}
|
|
250
|
-
|
|
250
|
+
const record = parseHmacKeyRecord(raw);
|
|
251
|
+
// Converge a stale-ACL'd hmackey to silent, on the HOT read path. An old helper
|
|
252
|
+
// (pre the metadata/hmackey no-ACL migration fix) re-stamped this
|
|
253
|
+
// contractually-no-ACL item with a biometry ACL, so the read just above pops the
|
|
254
|
+
// generic "Agents CLI needs to authenticate" sheet on EVERY hashed lookup — the
|
|
255
|
+
// `agents devices list` stats probe the SessionStart hook runs, and every other
|
|
256
|
+
// background hashed read. `maybeAutoRekey`'s one-shot heal only fires on a
|
|
257
|
+
// cleartext-bundle resolve and is bypassed for the hmackey/hashed-name path
|
|
258
|
+
// (prepareServiceName returns early for HMAC_KEY_ITEM before maybeAutoRekey), so
|
|
259
|
+
// it never converged exactly these reads and the machine prompted forever.
|
|
260
|
+
// Re-store the record no-ACL once per machine here (the read that produced it has
|
|
261
|
+
// already happened — and already prompted if the item was ACL'd); every
|
|
262
|
+
// subsequent read, in this process and all future ones, is silent.
|
|
263
|
+
if (record && !record.healedNoAcl) {
|
|
264
|
+
try {
|
|
265
|
+
healHmacKeyNoAclOnce(record);
|
|
266
|
+
record.healedNoAcl = true;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// A failed no-ACL re-store leaves the item still ACL'd (a still-prompting
|
|
270
|
+
// read) rather than a silent wrong state; the next process retries.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return record;
|
|
251
274
|
}
|
|
252
275
|
function writeHmacKeyRecord(rec) {
|
|
253
276
|
// JSON.stringify drops undefined fields (used to clear pendingDeletes).
|
|
@@ -423,19 +446,10 @@ function maybeAutoRekey() {
|
|
|
423
446
|
return;
|
|
424
447
|
const st = resolveHashState();
|
|
425
448
|
if (st.active) {
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
if (st.record && !st.record.healedNoAcl) {
|
|
431
|
-
try {
|
|
432
|
-
healHmacKeyNoAclOnce(st.record);
|
|
433
|
-
st.record.healedNoAcl = true;
|
|
434
|
-
}
|
|
435
|
-
catch {
|
|
436
|
-
/* next process retries */
|
|
437
|
-
}
|
|
438
|
-
}
|
|
449
|
+
// The stale-ACL'd-hmackey heal now runs on the hot read path
|
|
450
|
+
// (readHmacKeyRecord), which the resolveHashState() above just went through —
|
|
451
|
+
// so st.record is already healed here regardless of how this machine reached
|
|
452
|
+
// "hashing active". Nothing to do but finish any pending deletes.
|
|
439
453
|
if (st.record?.pendingDeletes?.length) {
|
|
440
454
|
try {
|
|
441
455
|
finishPendingDeletes(st.record);
|
|
@@ -46,7 +46,7 @@ export declare function resolveHostSshTarget(nameOrAlias: string): Promise<strin
|
|
|
46
46
|
* Merge `--host <single>` / `--hosts <a,b,c>` (and their `--device` / `--devices`
|
|
47
47
|
* aliases) into an ordered, de-duplicated list. All four flags compose; any alone
|
|
48
48
|
* works. `--device`/`--devices` resolve identically to `--host`/`--hosts` so the
|
|
49
|
-
* fleet-wide `--device` vocabulary (see `agents
|
|
49
|
+
* fleet-wide `--device` vocabulary (see `agents run --device`, `agents feed --host`)
|
|
50
50
|
* works on the secrets remote commands too. Empty when none is set.
|
|
51
51
|
*/
|
|
52
52
|
export declare function parseHostsOption(opts: {
|
|
@@ -77,7 +77,7 @@ export async function resolveHostSshTarget(nameOrAlias) {
|
|
|
77
77
|
* Merge `--host <single>` / `--hosts <a,b,c>` (and their `--device` / `--devices`
|
|
78
78
|
* aliases) into an ordered, de-duplicated list. All four flags compose; any alone
|
|
79
79
|
* works. `--device`/`--devices` resolve identically to `--host`/`--hosts` so the
|
|
80
|
-
* fleet-wide `--device` vocabulary (see `agents
|
|
80
|
+
* fleet-wide `--device` vocabulary (see `agents run --device`, `agents feed --host`)
|
|
81
81
|
* works on the secrets remote commands too. Empty when none is set.
|
|
82
82
|
*/
|
|
83
83
|
export function parseHostsOption(opts) {
|
|
@@ -58,6 +58,8 @@ export interface ActiveSession {
|
|
|
58
58
|
pid?: number;
|
|
59
59
|
sessionId?: string;
|
|
60
60
|
cwd?: string;
|
|
61
|
+
/** Project/repo key derived from cwd, when known. */
|
|
62
|
+
project?: string | null;
|
|
61
63
|
/** User-given name from /rename command. */
|
|
62
64
|
label?: string;
|
|
63
65
|
/** Durable `agents run --name` launch handle, when the run was named. */
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parses the raw command strings agents pass to Bash tool calls into structured
|
|
3
3
|
* metadata: the executable, category, subcommand, and a display summary. Used by
|
|
4
|
-
* session rendering and the activity-log hook so `agents sessions`
|
|
5
|
-
*
|
|
6
|
-
* wall of shell.
|
|
4
|
+
* session rendering and the activity-log hook so `agents sessions` can
|
|
5
|
+
* summarize what actually happened instead of printing a wall of shell.
|
|
7
6
|
*/
|
|
8
7
|
export type BashCategory = 'vcs' | 'build-test' | 'install' | 'remote' | 'http' | 'media' | 'upscaling' | 'metadata' | 'probe' | 'search' | 'shell' | 'wait' | 'other';
|
|
9
8
|
export interface BashToolInfo {
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -12,7 +12,14 @@ import { type IndexedToolCall } from './tool-calls.js';
|
|
|
12
12
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
13
13
|
* assert against the constant instead of hardcoding a number that every bump
|
|
14
14
|
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
15
|
-
export declare const SCHEMA_VERSION =
|
|
15
|
+
export declare const SCHEMA_VERSION = 31;
|
|
16
|
+
/**
|
|
17
|
+
* Bump to force `agents sessions backfill resources` to re-derive every
|
|
18
|
+
* session's skill/slash-command tallies on its next run (resource_scan_ledger
|
|
19
|
+
* rows with a lower version are treated as stale — the same mechanism
|
|
20
|
+
* TOOL_INDEX_VERSION gives the tool backfill).
|
|
21
|
+
*/
|
|
22
|
+
export declare const RESOURCE_INDEX_VERSION = 1;
|
|
16
23
|
/** Raw row shape returned from the sessions table. */
|
|
17
24
|
export interface SessionRow {
|
|
18
25
|
id: string;
|
|
@@ -75,6 +82,8 @@ export interface QueryOptions {
|
|
|
75
82
|
/** Match any session whose cwd equals this or is a descendant of it. */
|
|
76
83
|
cwdPrefix?: string;
|
|
77
84
|
project?: string;
|
|
85
|
+
/** Only sessions recorded on this machine (host), case-insensitive. */
|
|
86
|
+
machine?: string;
|
|
78
87
|
/** Match the full session id or short id, case-insensitively (exact). */
|
|
79
88
|
idExact?: string;
|
|
80
89
|
/** Match sessions whose id or short id begins with this (case-insensitive prefix). */
|
|
@@ -324,6 +333,88 @@ export declare function queryAffinityRollup(options: {
|
|
|
324
333
|
export declare function queryUsageRollup(options: QueryOptions & {
|
|
325
334
|
groupBy: UsageRollupGroup;
|
|
326
335
|
}): UsageRollupRow[];
|
|
336
|
+
/** One aggregated resource (skill or slash-command) in a usage-stats rollup. */
|
|
337
|
+
export interface ResourceStatRow {
|
|
338
|
+
/** 'skill' or 'command' (singular, as stored in session_resource_usage.kind). */
|
|
339
|
+
kind: string;
|
|
340
|
+
/** Stored resource name — bare, or `plugin:short` for a plugin-owned resource. */
|
|
341
|
+
name: string;
|
|
342
|
+
/** Owning plugin, or null for a flat (non-namespaced) resource. */
|
|
343
|
+
plugin: string | null;
|
|
344
|
+
/** DotAgents layer or plugin marketplace the resource resolved to at write time. */
|
|
345
|
+
source: string | null;
|
|
346
|
+
/** Distinct sessions that invoked this resource within the filter window. */
|
|
347
|
+
sessions: number;
|
|
348
|
+
/** Total invocations (sum of per-session counts) within the window. */
|
|
349
|
+
invocations: number;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Roll up skill / slash-command usage from session_resource_usage, joined to
|
|
353
|
+
* `sessions` for attribution so the same filter shape as querySessions
|
|
354
|
+
* (agent / project / since / machine) narrows WHICH sessions count. Grouped by
|
|
355
|
+
* resource identity (kind + name + plugin + source) and ordered by invocation
|
|
356
|
+
* volume — the read side of "which skills/commands do I actually use, and which
|
|
357
|
+
* are dead weight". `order: 'bottom'` ranks least-used first (the one-time
|
|
358
|
+
* skills); `limit` caps the returned rows.
|
|
359
|
+
*
|
|
360
|
+
* The signal only captures EXPLICIT invocations (slash commands and `Skill`
|
|
361
|
+
* tool calls). An auto-triggered skill (loaded by description match) emits no
|
|
362
|
+
* event, so it reads as zero here — a 0 means "never explicitly invoked", not
|
|
363
|
+
* "never loaded". Skill invocations are recorded for Claude and Kimi (the
|
|
364
|
+
* `Skill`-tool harnesses); slash-commands are Claude-only.
|
|
365
|
+
*
|
|
366
|
+
* `kind` / `pluginFilter` filter the RESOURCE rows directly (r.kind / r.plugin),
|
|
367
|
+
* distinct from QueryOptions.skill / QueryOptions.plugin, which filter SESSIONS
|
|
368
|
+
* — deliberately not routed through buildSessionWhere so `--plugin rush` shows
|
|
369
|
+
* only rush's resources rather than every resource used by a rush-touching
|
|
370
|
+
* session.
|
|
371
|
+
*/
|
|
372
|
+
export declare function queryResourceUsageStats(options: QueryOptions & {
|
|
373
|
+
kind?: 'skill' | 'command';
|
|
374
|
+
pluginFilter?: string;
|
|
375
|
+
order?: 'top' | 'bottom';
|
|
376
|
+
limit?: number;
|
|
377
|
+
}): ResourceStatRow[];
|
|
378
|
+
/**
|
|
379
|
+
* Coverage of the resource-usage signal: how many distinct sessions carry any
|
|
380
|
+
* row in session_resource_usage vs. the total indexed. A low ratio means the
|
|
381
|
+
* historical backfill (`agents sessions backfill resources`) hasn't run — the
|
|
382
|
+
* stats surface uses this to tell the user their zero-counts may just be
|
|
383
|
+
* un-scanned history, not genuine non-use.
|
|
384
|
+
*/
|
|
385
|
+
export declare function resourceUsageCoverage(): {
|
|
386
|
+
covered: number;
|
|
387
|
+
total: number;
|
|
388
|
+
};
|
|
389
|
+
/** Outcome of a resource-usage backfill run. */
|
|
390
|
+
export interface ResourceBackfillResult {
|
|
391
|
+
/** Sessions considered (matched the filter, had a real transcript). */
|
|
392
|
+
scanned: number;
|
|
393
|
+
/** Sessions (re)parsed and written this run. */
|
|
394
|
+
updated: number;
|
|
395
|
+
/** Sessions already current at this extractor version, skipped. */
|
|
396
|
+
skipped: number;
|
|
397
|
+
/** Sessions whose transcript could not be stat'd or parsed. */
|
|
398
|
+
failed: number;
|
|
399
|
+
/** Total session_resource_usage rows written across updated sessions. */
|
|
400
|
+
resourceRows: number;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* One-shot historical backfill of session_resource_usage (#12). The normal
|
|
404
|
+
* incremental scan writes resource usage only for sessions whose transcript it
|
|
405
|
+
* (re)parses; a session indexed before this feature shipped keeps a fresh
|
|
406
|
+
* scan_ledger row and is never re-derived, so its skill/slash-command tallies
|
|
407
|
+
* were never recorded. This walks the session index, re-parses each transcript
|
|
408
|
+
* FROM BYTE 0 (parseSession fully materializes — no resumable cursor, so
|
|
409
|
+
* claude/codex re-derive their tallies from scratch), writes the usage, and
|
|
410
|
+
* stamps resource_scan_ledger so reruns skip completed transcripts — the same
|
|
411
|
+
* independent-ledger shape `agents sessions backfill tools` uses.
|
|
412
|
+
*
|
|
413
|
+
* Harness-agnostic: parseSession + extractSkills/extractSlashCommands cover
|
|
414
|
+
* every agent uniformly, so there is no per-harness branch to keep in parity.
|
|
415
|
+
* Synthetic rows without a transcript (OpenClaw channels/cron) are skipped.
|
|
416
|
+
*/
|
|
417
|
+
export declare function backfillResourceUsage(filter?: QueryOptions, onProgress?: (done: number, total: number) => void): ResourceBackfillResult;
|
|
327
418
|
/** Who spawned a team: the orchestrator session, from its transcript. */
|
|
328
419
|
export interface TeamSpawner {
|
|
329
420
|
sessionId: string;
|