@phnx-labs/agents-cli 1.21.1 → 1.21.3
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 +206 -0
- package/README.md +1 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.d.ts +4 -0
- package/dist/commands/computer-actions.js +34 -0
- package/dist/commands/computer.js +4 -2
- package/dist/commands/doctor.js +5 -2
- package/dist/commands/feed.js +28 -19
- package/dist/commands/hooks.js +9 -45
- package/dist/commands/menubar.js +24 -24
- package/dist/commands/message.js +23 -3
- package/dist/commands/perf.d.ts +29 -0
- package/dist/commands/perf.js +90 -23
- package/dist/commands/projects.d.ts +21 -0
- package/dist/commands/projects.js +169 -23
- package/dist/commands/routines.js +46 -1
- package/dist/commands/sessions-picker.js +17 -2
- package/dist/commands/sessions.d.ts +5 -0
- package/dist/commands/sessions.js +14 -0
- package/dist/commands/ssh.js +69 -0
- package/dist/commands/trends.d.ts +2 -0
- package/dist/commands/trends.js +158 -0
- package/dist/commands/usage.d.ts +4 -4
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +90 -45
- package/dist/index.js +22 -1
- package/dist/lib/agents.js +2 -2
- package/dist/lib/analytics/dashboard.d.ts +11 -0
- package/dist/lib/analytics/dashboard.js +31 -0
- package/dist/lib/analytics/recipes.d.ts +32 -0
- package/dist/lib/analytics/recipes.js +316 -0
- package/dist/lib/analytics/usage-db.d.ts +84 -0
- package/dist/lib/analytics/usage-db.js +301 -0
- package/dist/lib/browser/service.js +31 -0
- package/dist/lib/cli-resources.d.ts +20 -0
- package/dist/lib/cli-resources.js +48 -1
- package/dist/lib/computer/dispatch.d.ts +3 -1
- package/dist/lib/computer/dispatch.js +10 -2
- package/dist/lib/daemon.js +51 -14
- package/dist/lib/devices/health-report.d.ts +5 -0
- package/dist/lib/devices/health-report.js +3 -0
- package/dist/lib/event-stream.d.ts +2 -0
- package/dist/lib/event-stream.js +3 -0
- package/dist/lib/events.d.ts +3 -1
- package/dist/lib/events.js +4 -2
- package/dist/lib/feed-broadcast.d.ts +52 -7
- package/dist/lib/feed-broadcast.js +125 -18
- package/dist/lib/fleet-cache.d.ts +37 -0
- package/dist/lib/fleet-cache.js +40 -0
- package/dist/lib/fleet-status.d.ts +53 -0
- package/dist/lib/fleet-status.js +120 -0
- package/dist/lib/friction-heuristics.d.ts +32 -0
- package/dist/lib/friction-heuristics.js +47 -0
- package/dist/lib/git.d.ts +14 -0
- package/dist/lib/git.js +36 -0
- package/dist/lib/hooks/cache.js +28 -6
- package/dist/lib/hooks/profile.d.ts +8 -0
- package/dist/lib/hooks/profile.js +15 -18
- package/dist/lib/hooks.js +72 -17
- package/dist/lib/linear-cache.d.ts +63 -0
- package/dist/lib/linear-cache.js +146 -0
- package/dist/lib/linear-project-counts.d.ts +35 -5
- package/dist/lib/linear-project-counts.js +61 -16
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +3 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +7 -0
- package/dist/lib/menubar/install-menubar.js +36 -6
- package/dist/lib/percentile.d.ts +12 -0
- package/dist/lib/percentile.js +24 -0
- package/dist/lib/perf/db.d.ts +7 -3
- package/dist/lib/perf/db.js +37 -19
- package/dist/lib/perf/types.d.ts +10 -0
- package/dist/lib/plugins.js +12 -1
- package/dist/lib/project-doctor.d.ts +36 -0
- package/dist/lib/project-doctor.js +45 -0
- package/dist/lib/project-import.d.ts +11 -1
- package/dist/lib/project-import.js +17 -3
- package/dist/lib/project-status.d.ts +25 -5
- package/dist/lib/project-status.js +48 -6
- package/dist/lib/resources.d.ts +16 -0
- package/dist/lib/resources.js +25 -14
- package/dist/lib/rotate.d.ts +27 -0
- package/dist/lib/rotate.js +44 -17
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +26 -0
- package/dist/lib/runner.js +41 -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/usage-db.d.ts +3 -63
- package/dist/lib/secrets/usage-db.js +46 -186
- package/dist/lib/session/db.d.ts +17 -1
- package/dist/lib/session/db.js +240 -6
- package/dist/lib/session/discover.d.ts +31 -0
- package/dist/lib/session/discover.js +50 -1
- package/dist/lib/session/highlights.d.ts +24 -4
- package/dist/lib/session/highlights.js +52 -7
- package/dist/lib/session/parse.js +19 -3
- package/dist/lib/session/prompt.d.ts +19 -0
- package/dist/lib/session/prompt.js +43 -0
- package/dist/lib/session/types.d.ts +43 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +31 -3
- package/dist/lib/state.js +53 -10
- package/dist/lib/types.d.ts +22 -4
- package/dist/lib/usage-refresh.d.ts +106 -0
- package/dist/lib/usage-refresh.js +238 -0
- package/dist/lib/usage.d.ts +152 -17
- package/dist/lib/usage.js +393 -79
- package/package.json +1 -1
package/dist/lib/resources.js
CHANGED
|
@@ -15,6 +15,16 @@ import { WorkflowsHandler } from './resources/workflows.js';
|
|
|
15
15
|
import { isCapable } from './capabilities.js';
|
|
16
16
|
import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, } from './state.js';
|
|
17
17
|
import { isNameActiveInResourceProfile } from './resource-profiles.js';
|
|
18
|
+
import { resolveSnapshotSha } from './git.js';
|
|
19
|
+
/** Build a ResolvedResource with a lazy, memoized `snapshotSha` getter. */
|
|
20
|
+
function withProvenance(base) {
|
|
21
|
+
return {
|
|
22
|
+
...base,
|
|
23
|
+
get snapshotSha() {
|
|
24
|
+
return resolveSnapshotSha(base.repoRoot);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
18
28
|
function profiledKind(kind) {
|
|
19
29
|
switch (kind) {
|
|
20
30
|
case 'commands':
|
|
@@ -70,19 +80,19 @@ export function resolveResource(kind, name, cwd) {
|
|
|
70
80
|
const projectDir = getProjectAgentsDir(cwd);
|
|
71
81
|
const extraRepos = getEnabledExtraRepos();
|
|
72
82
|
const candidates = [
|
|
73
|
-
...(projectDir ? [[path.join(projectDir, kind), 'project']] : []),
|
|
74
|
-
[path.join(getUserAgentsDir(), kind), 'user'],
|
|
75
|
-
[path.join(getSystemAgentsDir(), kind), 'system'],
|
|
76
|
-
...extraRepos.map((e) => [path.join(e.dir, kind), e.alias]),
|
|
83
|
+
...(projectDir ? [[path.join(projectDir, kind), 'project', projectDir]] : []),
|
|
84
|
+
[path.join(getUserAgentsDir(), kind), 'user', getUserAgentsDir()],
|
|
85
|
+
[path.join(getSystemAgentsDir(), kind), 'system', getSystemAgentsDir()],
|
|
86
|
+
...extraRepos.map((e) => [path.join(e.dir, kind), e.alias, e.dir]),
|
|
77
87
|
];
|
|
78
|
-
for (const [dir, source] of candidates) {
|
|
88
|
+
for (const [dir, source, repoRoot] of candidates) {
|
|
79
89
|
if (!fs.existsSync(dir))
|
|
80
90
|
continue;
|
|
81
91
|
// Try exact name (for directories like skills/subagents)
|
|
82
92
|
const exactPath = path.join(dir, name);
|
|
83
93
|
if (fs.existsSync(exactPath)) {
|
|
84
94
|
if (resourceIsActive(kind, name, source)) {
|
|
85
|
-
return { name, path: exactPath, source };
|
|
95
|
+
return withProvenance({ name, path: exactPath, source, repoRoot });
|
|
86
96
|
}
|
|
87
97
|
continue;
|
|
88
98
|
}
|
|
@@ -94,7 +104,7 @@ export function resolveResource(kind, name, cwd) {
|
|
|
94
104
|
const withExt = exactPath + ext;
|
|
95
105
|
if (fs.existsSync(withExt)) {
|
|
96
106
|
if (resourceIsActive(kind, name, source)) {
|
|
97
|
-
return { name, path: withExt, source };
|
|
107
|
+
return withProvenance({ name, path: withExt, source, repoRoot });
|
|
98
108
|
}
|
|
99
109
|
continue;
|
|
100
110
|
}
|
|
@@ -113,12 +123,12 @@ export function listResources(kind, cwd) {
|
|
|
113
123
|
const projectDir = getProjectAgentsDir(cwd);
|
|
114
124
|
const extraRepos = getEnabledExtraRepos();
|
|
115
125
|
const roots = [
|
|
116
|
-
...(projectDir ? [[path.join(projectDir, kind), 'project']] : []),
|
|
117
|
-
[path.join(getUserAgentsDir(), kind), 'user'],
|
|
118
|
-
[path.join(getSystemAgentsDir(), kind), 'system'],
|
|
119
|
-
...extraRepos.map((e) => [path.join(e.dir, kind), e.alias]),
|
|
126
|
+
...(projectDir ? [[path.join(projectDir, kind), 'project', projectDir]] : []),
|
|
127
|
+
[path.join(getUserAgentsDir(), kind), 'user', getUserAgentsDir()],
|
|
128
|
+
[path.join(getSystemAgentsDir(), kind), 'system', getSystemAgentsDir()],
|
|
129
|
+
...extraRepos.map((e) => [path.join(e.dir, kind), e.alias, e.dir]),
|
|
120
130
|
];
|
|
121
|
-
for (const [dir, source] of roots) {
|
|
131
|
+
for (const [dir, source, repoRoot] of roots) {
|
|
122
132
|
if (!fs.existsSync(dir))
|
|
123
133
|
continue;
|
|
124
134
|
let entries;
|
|
@@ -143,11 +153,12 @@ export function listResources(kind, cwd) {
|
|
|
143
153
|
if (!resourceIsActive(kind, rawName, source))
|
|
144
154
|
continue;
|
|
145
155
|
seen.add(rawName);
|
|
146
|
-
results.push({
|
|
156
|
+
results.push(withProvenance({
|
|
147
157
|
name: rawName,
|
|
148
158
|
path: path.join(dir, entry.name),
|
|
149
159
|
source,
|
|
150
|
-
|
|
160
|
+
repoRoot,
|
|
161
|
+
}));
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
164
|
return results;
|
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -24,6 +24,15 @@ export interface RotateCandidate {
|
|
|
24
24
|
usageStatus: AccountInfo['usageStatus'];
|
|
25
25
|
usageSnapshot: UsageSnapshot | null;
|
|
26
26
|
usageError: string | null;
|
|
27
|
+
/**
|
|
28
|
+
* Projected minutes until this account's 5-hour session window caps, as
|
|
29
|
+
* computed by the daemon's burn-rate refresher and read from the headroom
|
|
30
|
+
* cache. `null` when unknown (cold cache, idle, or not burning up). Balanced
|
|
31
|
+
* routing deprioritizes an account projected to cap soon — see
|
|
32
|
+
* {@link capacityWeight} — so a launch avoids an account racing toward its
|
|
33
|
+
* limit, not just one already 100%-maxed.
|
|
34
|
+
*/
|
|
35
|
+
usageMinutesToLimit: number | null;
|
|
27
36
|
plan: string | null;
|
|
28
37
|
signedIn: boolean;
|
|
29
38
|
lastActive: Date | null;
|
|
@@ -141,6 +150,24 @@ export declare function checkRunAccountReadiness(agent: AgentId, version: string
|
|
|
141
150
|
* version so behavior stays predictable.
|
|
142
151
|
*/
|
|
143
152
|
export declare function pickBalancedCandidate(candidates: RotateCandidate[], nowMs?: number): RotateResult | null;
|
|
153
|
+
/**
|
|
154
|
+
* How far from its projected cap an account must be to keep its FULL headroom
|
|
155
|
+
* weight. Inside this horizon the weight is scaled down linearly toward the
|
|
156
|
+
* floor, so an account racing toward its 5h cap loses priority before it maxes.
|
|
157
|
+
*/
|
|
158
|
+
export declare const PROJECTION_HORIZON_MIN = 30;
|
|
159
|
+
/**
|
|
160
|
+
* Weight one candidate by remaining routing capacity, deprioritized by how soon
|
|
161
|
+
* it is projected to cap. The base is weekly headroom (`max(1, 100 - used)`);
|
|
162
|
+
* an account with no live snapshot is treated as full-capacity (100) since there
|
|
163
|
+
* is no signal to deprioritize it. `minutesToLimit` (the daemon's burn-rate
|
|
164
|
+
* projection on the 5h session window) then scales that base: >= horizon (or
|
|
165
|
+
* unknown) keeps full weight, and closer-to-cap scales toward the floor of 1 —
|
|
166
|
+
* so a launch avoids an account projected to cap soon, not just a 100%-maxed
|
|
167
|
+
* one. Pure + exported so the deprioritization is unit-tested directly (a
|
|
168
|
+
* weighted-random draw is not).
|
|
169
|
+
*/
|
|
170
|
+
export declare function capacityWeight(usedPercent: number | null, minutesToLimit: number | null): number;
|
|
144
171
|
/**
|
|
145
172
|
* Pick an available candidate. Prefers the configured pinned version when that
|
|
146
173
|
* version has usage available; otherwise routes to the candidate with the most
|
package/dist/lib/rotate.js
CHANGED
|
@@ -12,6 +12,7 @@ import { listInstalledVersions, getVersionHomePath, resolveVersion } from './ver
|
|
|
12
12
|
import { getProjectRunConfigs } from './run-config.js';
|
|
13
13
|
import { emit } from './events.js';
|
|
14
14
|
import { getUsageInfoByIdentity, getUsageLookupKey, deriveUsageStatusFromSnapshot, } from './usage.js';
|
|
15
|
+
import { readAccountHeadroom } from './fleet-cache.js';
|
|
15
16
|
function getRotateDir() {
|
|
16
17
|
const dir = path.join(getHelpersDir(), 'rotate');
|
|
17
18
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -264,20 +265,37 @@ function preferVerified(pool, nowMs, choose) {
|
|
|
264
265
|
usageUnverified: verified.length === 0,
|
|
265
266
|
};
|
|
266
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* How far from its projected cap an account must be to keep its FULL headroom
|
|
270
|
+
* weight. Inside this horizon the weight is scaled down linearly toward the
|
|
271
|
+
* floor, so an account racing toward its 5h cap loses priority before it maxes.
|
|
272
|
+
*/
|
|
273
|
+
export const PROJECTION_HORIZON_MIN = 30;
|
|
274
|
+
/**
|
|
275
|
+
* Weight one candidate by remaining routing capacity, deprioritized by how soon
|
|
276
|
+
* it is projected to cap. The base is weekly headroom (`max(1, 100 - used)`);
|
|
277
|
+
* an account with no live snapshot is treated as full-capacity (100) since there
|
|
278
|
+
* is no signal to deprioritize it. `minutesToLimit` (the daemon's burn-rate
|
|
279
|
+
* projection on the 5h session window) then scales that base: >= horizon (or
|
|
280
|
+
* unknown) keeps full weight, and closer-to-cap scales toward the floor of 1 —
|
|
281
|
+
* so a launch avoids an account projected to cap soon, not just a 100%-maxed
|
|
282
|
+
* one. Pure + exported so the deprioritization is unit-tested directly (a
|
|
283
|
+
* weighted-random draw is not).
|
|
284
|
+
*/
|
|
285
|
+
export function capacityWeight(usedPercent, minutesToLimit) {
|
|
286
|
+
const base = usedPercent === null ? 100 : Math.max(1, 100 - usedPercent);
|
|
287
|
+
if (minutesToLimit === null || !Number.isFinite(minutesToLimit))
|
|
288
|
+
return base;
|
|
289
|
+
const factor = Math.max(0, Math.min(1, minutesToLimit / PROJECTION_HORIZON_MIN));
|
|
290
|
+
return Math.max(1, base * factor);
|
|
291
|
+
}
|
|
267
292
|
/**
|
|
268
293
|
* Pick one candidate from `sorted` using weights proportional to remaining
|
|
269
|
-
* routing capacity. Floor each weight at 1 so a
|
|
270
|
-
* eligible candidate can still be picked occasionally.
|
|
271
|
-
* (no live snapshot), treat the candidate as full-capacity (weight 100) — we
|
|
272
|
-
* have no signal to deprioritize it.
|
|
294
|
+
* routing capacity (see {@link capacityWeight}). Floor each weight at 1 so a
|
|
295
|
+
* near-exhausted-but-still-eligible candidate can still be picked occasionally.
|
|
273
296
|
*/
|
|
274
297
|
function weightedRandomByCapacity(sorted) {
|
|
275
|
-
const weights = sorted.map((c) =>
|
|
276
|
-
const used = getRoutingUsedPercent(c.usageSnapshot);
|
|
277
|
-
if (used === null)
|
|
278
|
-
return 100;
|
|
279
|
-
return Math.max(1, 100 - used);
|
|
280
|
-
});
|
|
298
|
+
const weights = sorted.map((c) => capacityWeight(getRoutingUsedPercent(c.usageSnapshot), c.usageMinutesToLimit));
|
|
281
299
|
const total = weights.reduce((sum, w) => sum + w, 0);
|
|
282
300
|
if (total <= 0)
|
|
283
301
|
return sorted[0];
|
|
@@ -350,26 +368,35 @@ export async function collectRunCandidates(agent) {
|
|
|
350
368
|
lastActive: info.lastActive,
|
|
351
369
|
};
|
|
352
370
|
}));
|
|
353
|
-
// These candidates feed a routing decision
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
371
|
+
// These candidates feed a routing decision on the `agents run` hot path, so
|
|
372
|
+
// this read is CACHE-ONLY (`readOnly`): it never blocks on a live provider
|
|
373
|
+
// fetch. A snapshot older than USAGE_DECISION_MAX_AGE_MS is not trusted for
|
|
374
|
+
// the pick — but the guard that enforces that is `isUsageVerified` below, not
|
|
375
|
+
// a blocking refresh here. Keeping the cache fresh is the daemon's job
|
|
376
|
+
// (`runUsageRefresh`, adaptive + rate-capped, sole-writer per local account),
|
|
377
|
+
// so a cold `agents run` reads the last daemon-written snapshot instead of
|
|
378
|
+
// stalling on N parallel HTTP round trips (the measured cold-start stall this
|
|
379
|
+
// removes). A stale-or-absent snapshot routes as unverified, exactly as a
|
|
380
|
+
// failed live read did before.
|
|
359
381
|
const { usageByKey } = await getUsageInfoByIdentity(rows.map(({ home, info, version }) => ({
|
|
360
382
|
agentId: agent,
|
|
361
383
|
home,
|
|
362
384
|
cliVersion: version,
|
|
363
385
|
info,
|
|
364
|
-
})), {
|
|
386
|
+
})), { readOnly: true });
|
|
365
387
|
return rows.map(({ home: _home, info, ...candidate }) => {
|
|
366
388
|
const usageKey = getUsageLookupKey(info);
|
|
367
389
|
const usage = usageKey ? usageByKey.get(usageKey) : undefined;
|
|
390
|
+
// Projected headroom is a separate cache-only read (also off the network) —
|
|
391
|
+
// the daemon publishes minutesToLimit; a cold cache yields null and routing
|
|
392
|
+
// falls back to snapshot-only weighting.
|
|
393
|
+
const headroom = usageKey ? readAccountHeadroom(usageKey) : null;
|
|
368
394
|
return {
|
|
369
395
|
...candidate,
|
|
370
396
|
usageKey,
|
|
371
397
|
usageSnapshot: usage?.snapshot ?? null,
|
|
372
398
|
usageError: usage?.error ?? null,
|
|
399
|
+
usageMinutesToLimit: headroom?.minutesToLimit ?? null,
|
|
373
400
|
};
|
|
374
401
|
});
|
|
375
402
|
}
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -504,6 +504,22 @@ export declare function getLatestRun(jobName: string): RunMeta | null;
|
|
|
504
504
|
* run's prompt.
|
|
505
505
|
*/
|
|
506
506
|
export declare function getLatestCompletedRun(jobName: string): RunMeta | null;
|
|
507
|
+
/** Duration + outcome rollup for a job's run history. */
|
|
508
|
+
export interface RoutineStats {
|
|
509
|
+
/** Total run records (any status, including `missed`). */
|
|
510
|
+
count: number;
|
|
511
|
+
failed: number;
|
|
512
|
+
missed: number;
|
|
513
|
+
avgMs: number;
|
|
514
|
+
p50: number;
|
|
515
|
+
p95: number;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Fold a job's run history (`listRuns`) into a duration + outcome summary.
|
|
519
|
+
* `missed` fires (no process ever ran) carry no `duration` and are excluded
|
|
520
|
+
* from the latency percentiles but still counted in `count`/`missed`.
|
|
521
|
+
*/
|
|
522
|
+
export declare function routineStats(jobName: string): RoutineStats;
|
|
507
523
|
/** Persist run metadata to its run directory as meta.json. */
|
|
508
524
|
export declare function writeRunMeta(meta: RunMeta): void;
|
|
509
525
|
/** Read run metadata from disk. Returns null if missing or corrupt. */
|
package/dist/lib/routines.js
CHANGED
|
@@ -16,6 +16,7 @@ import { atomicWriteFileSync } from './fs-atomic.js';
|
|
|
16
16
|
import { ALL_AGENT_IDS } from './agents.js';
|
|
17
17
|
import { machineId, normalizeHost } from './machine-id.js';
|
|
18
18
|
import { resolveActor } from './actor.js';
|
|
19
|
+
import { percentile } from './percentile.js';
|
|
19
20
|
export const HOST_STRATEGIES = ['local', 'host', 'fleet', 'cloud'];
|
|
20
21
|
/** Canonical set of accepted GitHub trigger events — single source for validation. */
|
|
21
22
|
export const GITHUB_TRIGGER_EVENTS = [
|
|
@@ -964,6 +965,31 @@ export function getLatestCompletedRun(jobName) {
|
|
|
964
965
|
}
|
|
965
966
|
return null;
|
|
966
967
|
}
|
|
968
|
+
/**
|
|
969
|
+
* Fold a job's run history (`listRuns`) into a duration + outcome summary.
|
|
970
|
+
* `missed` fires (no process ever ran) carry no `duration` and are excluded
|
|
971
|
+
* from the latency percentiles but still counted in `count`/`missed`.
|
|
972
|
+
*/
|
|
973
|
+
export function routineStats(jobName) {
|
|
974
|
+
const runs = listRuns(jobName);
|
|
975
|
+
const failed = runs.filter((r) => r.status === 'failed' || r.status === 'timeout').length;
|
|
976
|
+
const missed = runs.filter((r) => r.status === 'missed').length;
|
|
977
|
+
const durations = runs
|
|
978
|
+
.map((r) => r.duration)
|
|
979
|
+
.filter((d) => typeof d === 'number' && Number.isFinite(d))
|
|
980
|
+
.sort((a, b) => a - b);
|
|
981
|
+
const avgMs = durations.length > 0
|
|
982
|
+
? Math.round(durations.reduce((sum, d) => sum + d, 0) / durations.length)
|
|
983
|
+
: 0;
|
|
984
|
+
return {
|
|
985
|
+
count: runs.length,
|
|
986
|
+
failed,
|
|
987
|
+
missed,
|
|
988
|
+
avgMs,
|
|
989
|
+
p50: Math.round(percentile(durations, 50)),
|
|
990
|
+
p95: Math.round(percentile(durations, 95)),
|
|
991
|
+
};
|
|
992
|
+
}
|
|
967
993
|
/** Persist run metadata to its run directory as meta.json. */
|
|
968
994
|
export function writeRunMeta(meta) {
|
|
969
995
|
ensureAgentsDir();
|
package/dist/lib/runner.js
CHANGED
|
@@ -46,10 +46,35 @@ const AGENT_COMMANDS = {
|
|
|
46
46
|
/** Agents the daemon can actually run, derived from the command table above
|
|
47
47
|
* so the `--agent` help and any validation can never drift from it. */
|
|
48
48
|
export const ROUTINE_AGENT_IDS = Object.freeze(Object.keys(AGENT_COMMANDS));
|
|
49
|
+
/**
|
|
50
|
+
* Where each agent's transcript files live under an overlay HOME, mirroring
|
|
51
|
+
* `SESSION_ROOT_SPECS` (session/discover.ts) — the CLI's own source of truth
|
|
52
|
+
* for which on-disk trees hold live session files. Kept in this shape (not a
|
|
53
|
+
* shared import) because `archiveRoutineTranscripts` only needs a flat
|
|
54
|
+
* root+ext pair to `walkForFiles`, not the version-home/backup fan-out
|
|
55
|
+
* `getAgentSessionDirs` does for live discovery.
|
|
56
|
+
*
|
|
57
|
+
* `opencode` is deliberately absent: `SESSION_ROOT_SPECS` itself has no entry
|
|
58
|
+
* for it — its transcripts live in one incrementally-scanned SQLite db
|
|
59
|
+
* (`scanOpenCodeIncremental`), not a per-session file tree — so there is
|
|
60
|
+
* nothing here to mirror without inventing a new discovery path.
|
|
61
|
+
*/
|
|
49
62
|
const ROUTINE_TRANSCRIPT_SPECS = {
|
|
50
63
|
claude: [{ root: ['.claude', 'projects'], ext: '.jsonl' }],
|
|
51
64
|
codex: [{ root: ['.codex', 'sessions'], ext: '.jsonl' }],
|
|
52
65
|
cursor: [{ root: ['.cursor', 'projects'], ext: '.jsonl' }],
|
|
66
|
+
gemini: [{ root: ['.gemini', 'tmp'], ext: '.json' }],
|
|
67
|
+
antigravity: [{ root: ['.gemini', 'antigravity-cli', 'conversations'], ext: '.db' }],
|
|
68
|
+
droid: [{ root: ['.factory', 'sessions'], ext: '.jsonl' }],
|
|
69
|
+
// Kimi splits a session across two files (session/discover.ts:4382-4384):
|
|
70
|
+
// state.json (title/timestamps) and agents/main/wire.jsonl (the actual
|
|
71
|
+
// conversation). Both extensions are needed — .json alone archives only
|
|
72
|
+
// the metadata shell and silently drops every message.
|
|
73
|
+
kimi: [
|
|
74
|
+
{ root: ['.kimi-code', 'sessions'], ext: '.json' },
|
|
75
|
+
{ root: ['.kimi-code', 'sessions'], ext: '.jsonl' },
|
|
76
|
+
],
|
|
77
|
+
grok: [{ root: ['.grok', 'sessions'], ext: '.json' }],
|
|
53
78
|
};
|
|
54
79
|
/** Stable working directory for routine children, independent of the daemon's launch cwd. */
|
|
55
80
|
export function routineSpawnCwd(config, configuredRoot = getProjectRoot()) {
|
|
@@ -1080,6 +1105,14 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1080
1105
|
// wait); the next schedule tick re-selects if this attempt still fails.
|
|
1081
1106
|
const launch = await resolveRoutineLaunch(config);
|
|
1082
1107
|
const version = launch.chain[0]?.version ?? config.version;
|
|
1108
|
+
const timer = createTimer('agent.run', {
|
|
1109
|
+
agent: config.agent,
|
|
1110
|
+
version,
|
|
1111
|
+
jobName: config.name,
|
|
1112
|
+
mode: config.mode,
|
|
1113
|
+
...redactPrompt(config.prompt),
|
|
1114
|
+
schedule: config.schedule,
|
|
1115
|
+
});
|
|
1083
1116
|
const resolvedPrompt = resolveJobPrompt(config);
|
|
1084
1117
|
let cmd = buildJobCommand(config, resolvedPrompt);
|
|
1085
1118
|
// workflow AND resume dispatch through `agents run` — never binary-pin them (pinning
|
|
@@ -1143,6 +1176,7 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1143
1176
|
finalizeRunMeta(meta, 'failed', 1, { errorMessage: reason });
|
|
1144
1177
|
writeRunMeta(meta);
|
|
1145
1178
|
archiveRoutineTranscripts(meta, runDir, overlayHome);
|
|
1179
|
+
timer.end({ status: 'failed', exitCode: 1, runId, error: reason });
|
|
1146
1180
|
return meta;
|
|
1147
1181
|
}
|
|
1148
1182
|
}
|
|
@@ -1164,6 +1198,7 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1164
1198
|
const isAuthFailure = !!errorMessage && errorMessage.startsWith('auth_failed:');
|
|
1165
1199
|
if (status !== 'timeout' && !isAuthFailure)
|
|
1166
1200
|
extractAndSaveReport(stdoutPath, effectiveAgent, runDir);
|
|
1201
|
+
timer.end({ status, exitCode: exitCode ?? undefined, runId, ...(errorMessage ? { error: errorMessage } : {}) });
|
|
1167
1202
|
// Fire the finish/output notification AFTER the report is written so the hook
|
|
1168
1203
|
// can read report.md (RUSH-2030). Best-effort; never breaks finalization.
|
|
1169
1204
|
safeHook(hooks?.onFinish ? () => hooks.onFinish(meta) : undefined);
|
|
@@ -1214,6 +1249,11 @@ export async function executeJobDetached(config, hooks) {
|
|
|
1214
1249
|
* `monitorRunningJobs` reaps the record on the next tick.
|
|
1215
1250
|
*/
|
|
1216
1251
|
function executeCommandJobDetached(config, hooks) {
|
|
1252
|
+
const timer = createTimer('agent.run', {
|
|
1253
|
+
jobName: config.name,
|
|
1254
|
+
mode: config.mode,
|
|
1255
|
+
schedule: config.schedule,
|
|
1256
|
+
});
|
|
1217
1257
|
const runId = generateRunId();
|
|
1218
1258
|
const runDir = getRunDir(config.name, runId);
|
|
1219
1259
|
fs.mkdirSync(runDir, { recursive: true });
|
|
@@ -1259,6 +1299,7 @@ function executeCommandJobDetached(config, hooks) {
|
|
|
1259
1299
|
settled = true;
|
|
1260
1300
|
finalizeRunMeta(meta, status, exitCode, errorMessage ? { errorMessage } : undefined);
|
|
1261
1301
|
writeRunMeta(meta);
|
|
1302
|
+
timer.end({ status, exitCode, runId, ...(errorMessage ? { error: errorMessage } : {}) });
|
|
1262
1303
|
// Finish notification (RUSH-2030). For command routines the threshold only
|
|
1263
1304
|
// surfaces failures, decided in routine-notify.ts. Best-effort.
|
|
1264
1305
|
safeHook(hooks?.onFinish ? () => hooks.onFinish(meta) : undefined);
|
|
Binary file
|
|
Binary file
|
|
@@ -1,69 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* value-free row for every secret lifecycle/access event a bundle accrues over
|
|
6
|
-
* its life — created, imported, exported, viewed, accessed (read for injection),
|
|
7
|
-
* unlocked. It is the queryable, per-bundle counterpart to the append-only
|
|
8
|
-
* ~/.agents/events.jsonl audit log: the SAME chokepoint (`emitSecretAudit`,
|
|
9
|
-
* lib/secrets/audit.ts) feeds both, and this store answers "how often / how
|
|
10
|
-
* recently / by whom was THIS bundle used?" without scanning the whole event
|
|
11
|
-
* stream. It is a DERIVED index fed off the real access flow — the way
|
|
12
|
-
* sessions.db indexes session metadata — never a second write path an operation
|
|
13
|
-
* has to remember to call.
|
|
14
|
-
*
|
|
15
|
-
* Contract, mirroring the audit log: NEVER a secret value. Only metadata — the
|
|
16
|
-
* bundle name, the event kind, key counts, the resolving agent/host, a status.
|
|
17
|
-
*
|
|
18
|
-
* Every write is best-effort: a failure here (missing runtime SQLite, a locked
|
|
19
|
-
* db, a read-only fs) is swallowed so usage telemetry can never break secret
|
|
20
|
-
* resolution. Set AGENTS_NO_USAGE_TRACK=1 to disable recording entirely (used by
|
|
21
|
-
* tests and by callers that must stay perfectly silent).
|
|
2
|
+
* Secrets usage read-model — thin adapter over the analytics usage warehouse
|
|
3
|
+
* (`kind=secret`). Kept so `secrets view` / `list` / `activity` keep their API
|
|
4
|
+
* while the durable store lives at ~/.agents/.history/analytics/usage.db.
|
|
22
5
|
*/
|
|
23
|
-
/** The lifecycle/access events a bundle accrues over its life. */
|
|
24
6
|
export type SecretUsageEvent = 'access' | 'unlock' | 'import' | 'export' | 'create' | 'view';
|
|
25
|
-
/** All event kinds, in the order `view` prints them. */
|
|
26
7
|
export declare const SECRET_USAGE_EVENTS: readonly SecretUsageEvent[];
|
|
27
8
|
export interface RecordUsageParams {
|
|
28
|
-
/** Bundle the event applies to (required — usage is always per-bundle). */
|
|
29
9
|
bundle: string;
|
|
30
|
-
/** What happened. */
|
|
31
10
|
event: SecretUsageEvent;
|
|
32
|
-
/** Resolving agent/harness identity, when known (`*` = a global grant). */
|
|
33
11
|
agent?: string;
|
|
34
|
-
/** Remote host the value was pulled from / pushed to, when applicable. */
|
|
35
12
|
host?: string;
|
|
36
|
-
/** Free-form origin label, e.g. 'agent', 'reveal', 'ssh', '1password'. */
|
|
37
13
|
source?: string;
|
|
38
|
-
/** Outcome; defaults to 'success'. */
|
|
39
14
|
status?: 'success' | 'error';
|
|
40
|
-
/** How many keys the event touched (names only are ever known here). */
|
|
41
15
|
keyCount?: number;
|
|
42
16
|
}
|
|
43
|
-
/** One event kind's rollup for a bundle. */
|
|
44
17
|
export interface UsageStat {
|
|
45
18
|
count: number;
|
|
46
|
-
/** ISO 8601 timestamp of the most recent occurrence, or null if never. */
|
|
47
19
|
last: string | null;
|
|
48
20
|
}
|
|
49
|
-
/** Per-bundle usage summary for the `view` / `list` surfaces. */
|
|
50
21
|
export interface BundleUsageSummary {
|
|
51
22
|
bundle: string;
|
|
52
|
-
/** Every recorded event, all kinds. */
|
|
53
23
|
total: number;
|
|
54
|
-
/** Rollup per event kind (every kind present, zeroed when unused). */
|
|
55
24
|
events: Record<SecretUsageEvent, UsageStat>;
|
|
56
|
-
/** Most recent event across all kinds, or null. */
|
|
57
25
|
lastUsedAt: string | null;
|
|
58
|
-
/** Earliest event across all kinds, or null. */
|
|
59
26
|
firstUsedAt: string | null;
|
|
60
|
-
/** Event count grouped by resolving agent, most-first. `*` = a global grant. */
|
|
61
27
|
byAgent: Array<{
|
|
62
28
|
agent: string;
|
|
63
29
|
count: number;
|
|
64
30
|
}>;
|
|
65
31
|
}
|
|
66
|
-
/** One recorded event, for the `secrets activity` timeline. */
|
|
67
32
|
export interface SecretUsageHistoryEntry {
|
|
68
33
|
ts: string;
|
|
69
34
|
bundle: string;
|
|
@@ -74,33 +39,8 @@ export interface SecretUsageHistoryEntry {
|
|
|
74
39
|
status: string | null;
|
|
75
40
|
keyCount: number | null;
|
|
76
41
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Record one usage event. Best-effort and value-free — swallows every error and
|
|
79
|
-
* honors AGENTS_NO_USAGE_TRACK so telemetry never blocks or slows a read. Rows
|
|
80
|
-
* with an empty bundle name are ignored (usage is per-bundle by definition).
|
|
81
|
-
*
|
|
82
|
-
* This is called from ONE place only — `emitSecretAudit` (lib/secrets/audit.ts)
|
|
83
|
-
* — so every recorded event has already been written to the events.jsonl audit
|
|
84
|
-
* log through the same chokepoint. Do not call it from a command handler; emit
|
|
85
|
-
* the audit event instead.
|
|
86
|
-
*/
|
|
87
42
|
export declare function recordSecretUsage(p: RecordUsageParams): void;
|
|
88
|
-
/**
|
|
89
|
-
* Usage summary for one bundle, or undefined when nothing has ever been
|
|
90
|
-
* recorded (or the DB is unavailable). Never throws.
|
|
91
|
-
*/
|
|
92
43
|
export declare function getBundleUsage(bundle: string): BundleUsageSummary | undefined;
|
|
93
|
-
/**
|
|
94
|
-
* Usage summaries for every bundle that has any recorded event, keyed by bundle
|
|
95
|
-
* name. Powers `secrets list --sort uses|used`. Empty map when the DB is
|
|
96
|
-
* unavailable or has no rows. Never throws.
|
|
97
|
-
*/
|
|
98
44
|
export declare function getAllBundleUsage(): Map<string, BundleUsageSummary>;
|
|
99
|
-
/**
|
|
100
|
-
* Recent events for the `secrets activity` timeline — one bundle when named,
|
|
101
|
-
* else across all bundles — newest first. Empty when the DB is unavailable.
|
|
102
|
-
* Never throws.
|
|
103
|
-
*/
|
|
104
45
|
export declare function getUsageHistory(bundle: string | undefined, limit?: number): SecretUsageHistoryEntry[];
|
|
105
|
-
/** Close the cached handle. Used by tests between temp-db swaps. */
|
|
106
46
|
export declare function closeSecretsUsageDb(): void;
|