@phnx-labs/agents-cli 1.22.26 → 1.22.28
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 +293 -0
- package/README.md +80 -27
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +71 -26
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +73 -7
- package/dist/commands/sessions.js +391 -49
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +22 -11
- package/dist/index.js +16 -20
- package/dist/lib/activity.js +45 -99
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +56 -3
- package/dist/lib/cloud/host.js +1 -0
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +14 -7
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +85 -25
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +27 -0
- package/dist/lib/hosts/passthrough.js +19 -10
- package/dist/lib/hosts/progress.d.ts +12 -1
- package/dist/lib/hosts/progress.js +41 -8
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +45 -4
- package/dist/lib/hosts/ready.js +124 -20
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +24 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +94 -36
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +4 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +3 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/menubar/snapshot.js +15 -2
- package/dist/lib/models.d.ts +42 -6
- package/dist/lib/models.js +55 -153
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/redact.js +8 -3
- package/dist/lib/refresh.js +22 -11
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -0
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +19 -3
- package/dist/lib/secrets/agent.js +61 -22
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +23 -2
- package/dist/lib/session/db.js +86 -20
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-bundle.js +1 -1
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
package/dist/commands/output.js
CHANGED
|
@@ -12,6 +12,7 @@ import { terminalWidth, truncateToWidth, padToWidth } from '../lib/session/width
|
|
|
12
12
|
import { collectGitOutput } from '../lib/output/git-output.js';
|
|
13
13
|
import { loadDevices, isControlDevice } from '../lib/devices/registry.js';
|
|
14
14
|
import { machineId } from '../lib/session/sync/config.js';
|
|
15
|
+
import { stripClixml } from '../lib/hosts/remote-cmd.js';
|
|
15
16
|
const execFileAsync = promisify(execFile);
|
|
16
17
|
export function registerOutputCommand(program) {
|
|
17
18
|
addHostOption(program.command('output'))
|
|
@@ -24,16 +25,21 @@ export function registerOutputCommand(program) {
|
|
|
24
25
|
.option('--login <login...>', 'Count PRs for these GitHub logins (default: current gh user)')
|
|
25
26
|
.option('--no-prs', 'Skip the GitHub PR lookup (commits only)')
|
|
26
27
|
.option('--all-hosts', 'Aggregate across every online device (ag devices) over SSH')
|
|
28
|
+
.option('--pricing <scenario>', 'Cost scenario: actual (default, cache-discounted) or no-cache (cache read/write billed at the full input rate)')
|
|
27
29
|
.addHelpText('after', `
|
|
28
30
|
Examples:
|
|
29
31
|
agents output Last 7 days: burn, output tokens, PRs, commits, ratios
|
|
30
32
|
agents output --since 24h Last 24 hours
|
|
31
33
|
agents output --since 1mo Last month (units: 1h 24h 7d 4w 1mo 1y, or ISO date)
|
|
34
|
+
agents output --pricing no-cache Model the burn as if prompt caching were off
|
|
32
35
|
agents output --all-hosts Fleet-wide, folding in every online machine
|
|
33
36
|
agents output --by day --json Machine-readable daily burn/output rollup
|
|
34
37
|
|
|
35
38
|
Burn (cost) is computed offline from a versioned per-model price table (${PRICING_VERSION}).
|
|
36
39
|
Output tokens are the real generated tokens — NOT the cache-inflated total token count.
|
|
40
|
+
The burn is split into input / cache-read / cache-write where the harness records it
|
|
41
|
+
(Claude/Codex/Gemini/Droid). --pricing no-cache reprices cached tokens at the input rate,
|
|
42
|
+
so you can see what caching is saving. --json always carries both actual and no-cache costs.
|
|
37
43
|
`)
|
|
38
44
|
.action(async (options) => {
|
|
39
45
|
await outputAction(options);
|
|
@@ -47,6 +53,14 @@ function resolveGroup(by) {
|
|
|
47
53
|
console.error(chalk.red('error: --by must be one of: agent, project, day, account'));
|
|
48
54
|
process.exit(1);
|
|
49
55
|
}
|
|
56
|
+
function resolvePricing(pricing) {
|
|
57
|
+
if (pricing === undefined || pricing === 'actual')
|
|
58
|
+
return 'actual';
|
|
59
|
+
if (pricing === 'no-cache')
|
|
60
|
+
return 'no-cache';
|
|
61
|
+
console.error(chalk.red('error: --pricing must be one of: actual, no-cache'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
50
64
|
/** Compact token formatter: 38.6M, 4.1K, 10.6B. */
|
|
51
65
|
function formatCompact(n) {
|
|
52
66
|
const abs = Math.abs(n);
|
|
@@ -59,13 +73,27 @@ function formatCompact(n) {
|
|
|
59
73
|
return String(n);
|
|
60
74
|
}
|
|
61
75
|
function emptyBurn() {
|
|
62
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
costUsd: 0,
|
|
78
|
+
costUsdNoCache: 0,
|
|
79
|
+
outputTokens: 0,
|
|
80
|
+
tokenCount: 0,
|
|
81
|
+
inputTokens: 0,
|
|
82
|
+
cacheReadTokens: 0,
|
|
83
|
+
cacheWriteTokens: 0,
|
|
84
|
+
sessionCount: 0,
|
|
85
|
+
durationMs: 0,
|
|
86
|
+
};
|
|
63
87
|
}
|
|
64
88
|
function sumBurn(rows) {
|
|
65
89
|
return rows.reduce((acc, r) => {
|
|
66
90
|
acc.costUsd += r.costUsd;
|
|
91
|
+
acc.costUsdNoCache += r.costUsdNoCache;
|
|
67
92
|
acc.outputTokens += r.outputTokens;
|
|
68
93
|
acc.tokenCount += r.tokenCount;
|
|
94
|
+
acc.inputTokens += r.inputTokens;
|
|
95
|
+
acc.cacheReadTokens += r.cacheReadTokens;
|
|
96
|
+
acc.cacheWriteTokens += r.cacheWriteTokens;
|
|
69
97
|
acc.sessionCount += r.sessionCount;
|
|
70
98
|
acc.durationMs += r.durationMs;
|
|
71
99
|
return acc;
|
|
@@ -122,7 +150,9 @@ async function fetchRemotePayload(device, options) {
|
|
|
122
150
|
timeout: 120_000,
|
|
123
151
|
maxBuffer: 64 * 1024 * 1024,
|
|
124
152
|
});
|
|
125
|
-
|
|
153
|
+
// A Windows device relays its payload through PowerShell, which can prefix a
|
|
154
|
+
// CLIXML banner ahead of the JSON — strip it before parsing (RUSH-2286).
|
|
155
|
+
const parsed = JSON.parse(stripClixml(stdout));
|
|
126
156
|
parsed.machine = parsed.machine || device;
|
|
127
157
|
return parsed;
|
|
128
158
|
}
|
|
@@ -141,13 +171,17 @@ async function fetchRemotePayload(device, options) {
|
|
|
141
171
|
}
|
|
142
172
|
async function outputAction(options) {
|
|
143
173
|
const includePrs = options.prs !== false;
|
|
174
|
+
// Validate --pricing up front so a bad value errors even under --json. The JSON
|
|
175
|
+
// payload always carries BOTH costs regardless of the scenario — the flag only
|
|
176
|
+
// chooses which one the text renderer leads with.
|
|
177
|
+
const scenario = resolvePricing(options.pricing);
|
|
144
178
|
if (!options.allHosts) {
|
|
145
179
|
const payload = await computeLocalPayload(options, includePrs);
|
|
146
180
|
if (options.json) {
|
|
147
181
|
process.stdout.write(JSON.stringify(withRatios(payload, [payload]), null, 2) + '\n');
|
|
148
182
|
return;
|
|
149
183
|
}
|
|
150
|
-
renderSingle(payload);
|
|
184
|
+
renderSingle(payload, scenario);
|
|
151
185
|
return;
|
|
152
186
|
}
|
|
153
187
|
// Fleet: local payload + every online device, folded in over SSH.
|
|
@@ -166,7 +200,7 @@ async function outputAction(options) {
|
|
|
166
200
|
process.stdout.write(JSON.stringify(withRatios(mergeMachines(machines, options), machines), null, 2) + '\n');
|
|
167
201
|
return;
|
|
168
202
|
}
|
|
169
|
-
renderFleet(machines, options);
|
|
203
|
+
renderFleet(machines, options, scenario);
|
|
170
204
|
}
|
|
171
205
|
/** Merge per-machine payloads into a combined one (burn + commits summed; PRs local-only). */
|
|
172
206
|
function mergeMachines(machines, options) {
|
|
@@ -179,8 +213,12 @@ function mergeMachines(machines, options) {
|
|
|
179
213
|
const uncosted = new Set();
|
|
180
214
|
for (const m of machines) {
|
|
181
215
|
burn.costUsd += m.burn.costUsd;
|
|
216
|
+
burn.costUsdNoCache += m.burn.costUsdNoCache;
|
|
182
217
|
burn.outputTokens += m.burn.outputTokens;
|
|
183
218
|
burn.tokenCount += m.burn.tokenCount;
|
|
219
|
+
burn.inputTokens += m.burn.inputTokens;
|
|
220
|
+
burn.cacheReadTokens += m.burn.cacheReadTokens;
|
|
221
|
+
burn.cacheWriteTokens += m.burn.cacheWriteTokens;
|
|
184
222
|
burn.sessionCount += m.burn.sessionCount;
|
|
185
223
|
burn.durationMs += m.burn.durationMs;
|
|
186
224
|
for (const s of m.output.commitShas)
|
|
@@ -188,15 +226,23 @@ function mergeMachines(machines, options) {
|
|
|
188
226
|
for (const a of m.uncostedAgents)
|
|
189
227
|
uncosted.add(a);
|
|
190
228
|
for (const r of m.breakdown.rows) {
|
|
191
|
-
const cur = byKey.get(r.key) ?? {
|
|
229
|
+
const cur = byKey.get(r.key) ?? {
|
|
230
|
+
key: r.key, label: r.label, costUsd: 0, costUsdNoCache: 0, durationMs: 0,
|
|
231
|
+
sessionCount: 0, tokenCount: 0, outputTokens: 0,
|
|
232
|
+
inputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
|
|
233
|
+
};
|
|
192
234
|
// Peers resolve their own labels; keep the first non-empty one so a machine that
|
|
193
235
|
// has not indexed an account yet does not blank a label another machine supplied.
|
|
194
236
|
cur.label ??= r.label;
|
|
195
237
|
cur.costUsd += r.costUsd;
|
|
238
|
+
cur.costUsdNoCache += r.costUsdNoCache;
|
|
196
239
|
cur.durationMs += r.durationMs;
|
|
197
240
|
cur.sessionCount += r.sessionCount;
|
|
198
241
|
cur.tokenCount += r.tokenCount;
|
|
199
242
|
cur.outputTokens += r.outputTokens;
|
|
243
|
+
cur.inputTokens += r.inputTokens;
|
|
244
|
+
cur.cacheReadTokens += r.cacheReadTokens;
|
|
245
|
+
cur.cacheWriteTokens += r.cacheWriteTokens;
|
|
200
246
|
byKey.set(r.key, cur);
|
|
201
247
|
}
|
|
202
248
|
}
|
|
@@ -226,47 +272,76 @@ function withRatios(payload, machines) {
|
|
|
226
272
|
machines: machines.length > 1 ? machines.map(m => ({ machine: m.machine, burn: m.burn, commits: m.output.commits, error: m.error })) : undefined,
|
|
227
273
|
};
|
|
228
274
|
}
|
|
229
|
-
/**
|
|
230
|
-
function
|
|
275
|
+
/** The cost the text renderer leads with for the chosen scenario. */
|
|
276
|
+
function scenarioCost(x, scenario) {
|
|
277
|
+
return scenario === 'no-cache' ? x.costUsdNoCache : x.costUsd;
|
|
278
|
+
}
|
|
279
|
+
/** Shared header line: burned · output tokens · PRs · commits, plus ratios + burn split. */
|
|
280
|
+
function headerLines(payload, scenario) {
|
|
231
281
|
const prsTotal = payload.output.prsOpened + payload.output.prsMerged;
|
|
282
|
+
const burn = payload.burn;
|
|
283
|
+
const cost = scenarioCost(burn, scenario);
|
|
232
284
|
const out = [];
|
|
233
285
|
out.push(' ' +
|
|
234
|
-
`${chalk.green(formatUsd(
|
|
286
|
+
`${chalk.green(formatUsd(cost))} burned${scenario === 'no-cache' ? chalk.gray(' (no-cache)') : ''}` +
|
|
235
287
|
chalk.gray(' · ') +
|
|
236
|
-
`${chalk.cyan(formatCompact(
|
|
288
|
+
`${chalk.cyan(formatCompact(burn.outputTokens))} output tokens` +
|
|
237
289
|
chalk.gray(' · ') +
|
|
238
290
|
`${chalk.yellow(String(prsTotal))} PRs ${chalk.gray(`(${payload.output.prsMerged} merged)`)}` +
|
|
239
291
|
chalk.gray(' · ') +
|
|
240
292
|
`${chalk.yellow(String(payload.output.commits))} commits`);
|
|
241
293
|
const ratios = [];
|
|
242
294
|
if (prsTotal > 0)
|
|
243
|
-
ratios.push(`${formatUsd(
|
|
295
|
+
ratios.push(`${formatUsd(cost / prsTotal)}/PR`);
|
|
244
296
|
if (payload.output.commits > 0)
|
|
245
|
-
ratios.push(`${formatUsd(
|
|
246
|
-
if (
|
|
247
|
-
ratios.push(`${formatCompact(Math.round(
|
|
297
|
+
ratios.push(`${formatUsd(cost / payload.output.commits)}/commit`);
|
|
298
|
+
if (cost > 0)
|
|
299
|
+
ratios.push(`${formatCompact(Math.round(burn.outputTokens / cost))} out-tok/$`);
|
|
248
300
|
if (ratios.length > 0)
|
|
249
301
|
out.push(chalk.gray(' ' + ratios.join(' · ')));
|
|
302
|
+
// Burn split — only for harnesses that recorded one (input/cache totals > 0).
|
|
303
|
+
const splitTotal = burn.inputTokens + burn.cacheReadTokens + burn.cacheWriteTokens;
|
|
304
|
+
if (splitTotal > 0) {
|
|
305
|
+
out.push(chalk.gray(' burn split: ') +
|
|
306
|
+
`${chalk.cyan(formatCompact(burn.inputTokens))} input` +
|
|
307
|
+
chalk.gray(' · ') +
|
|
308
|
+
`${chalk.cyan(formatCompact(burn.cacheReadTokens))} cache-read` +
|
|
309
|
+
chalk.gray(' · ') +
|
|
310
|
+
`${chalk.cyan(formatCompact(burn.cacheWriteTokens))} cache-write`);
|
|
311
|
+
}
|
|
312
|
+
// No-cache comparison — shown whenever caching actually moved the number, so an
|
|
313
|
+
// operator sees the saving in `actual` mode too (RUSH-2287: "both if useful").
|
|
314
|
+
if (burn.costUsdNoCache > burn.costUsd && burn.costUsd > 0) {
|
|
315
|
+
const saved = burn.costUsdNoCache - burn.costUsd;
|
|
316
|
+
const pct = Math.round((saved / burn.costUsdNoCache) * 100);
|
|
317
|
+
out.push(chalk.gray(' caching: ') +
|
|
318
|
+
`actual ${chalk.green(formatUsd(burn.costUsd))}` +
|
|
319
|
+
chalk.gray(' vs ') +
|
|
320
|
+
`no-cache ${chalk.yellow(formatUsd(burn.costUsdNoCache))}` +
|
|
321
|
+
chalk.gray(` (saved ${formatUsd(saved)}, ${pct}%)`));
|
|
322
|
+
}
|
|
250
323
|
return out;
|
|
251
324
|
}
|
|
252
325
|
/** Render the per-group burn/output table. */
|
|
253
|
-
function renderBreakdown(rows, groupBy) {
|
|
326
|
+
function renderBreakdown(rows, groupBy, scenario) {
|
|
254
327
|
const out = [chalk.bold(`By ${groupBy}`)];
|
|
255
328
|
if (rows.length === 0)
|
|
256
329
|
return out;
|
|
330
|
+
const rowCost = (r) => scenarioCost(r, scenario);
|
|
257
331
|
const cols = terminalWidth();
|
|
258
|
-
const
|
|
332
|
+
const burnHeader = scenario === 'no-cache' ? 'burn(nc)' : 'burn';
|
|
333
|
+
const burnW = Math.max(...rows.map(r => formatUsd(rowCost(r)).length), burnHeader.length);
|
|
259
334
|
const outW = Math.max(...rows.map(r => formatCompact(r.outputTokens).length), 6);
|
|
260
335
|
const sessW = Math.max(...rows.map(r => String(r.sessionCount).length), 3);
|
|
261
336
|
const fixedW = 2 + 2 + burnW + 2 + outW + 2 + sessW + 8;
|
|
262
337
|
const display = (r) => r.label ?? r.key;
|
|
263
338
|
const keyW = Math.max(8, Math.min(Math.max(...rows.map(r => display(r).length), groupBy.length), cols - fixedW));
|
|
264
|
-
out.push(' ' + chalk.gray(padToWidth('', keyW)) + ' ' + chalk.gray(padToWidth(
|
|
339
|
+
out.push(' ' + chalk.gray(padToWidth('', keyW)) + ' ' + chalk.gray(padToWidth(burnHeader, burnW)) + ' ' + chalk.gray(padToWidth('output', outW)) + ' ' + chalk.gray('sessions'));
|
|
265
340
|
for (const r of rows) {
|
|
266
341
|
out.push(' ' +
|
|
267
342
|
padToWidth(truncateToWidth(display(r), keyW), keyW) +
|
|
268
343
|
' ' +
|
|
269
|
-
chalk.green(padToWidth(formatUsd(r
|
|
344
|
+
chalk.green(padToWidth(formatUsd(rowCost(r)), burnW)) +
|
|
270
345
|
' ' +
|
|
271
346
|
chalk.cyan(padToWidth(formatCompact(r.outputTokens), outW)) +
|
|
272
347
|
' ' +
|
|
@@ -274,17 +349,17 @@ function renderBreakdown(rows, groupBy) {
|
|
|
274
349
|
}
|
|
275
350
|
return out;
|
|
276
351
|
}
|
|
277
|
-
function renderSingle(payload) {
|
|
352
|
+
function renderSingle(payload, scenario) {
|
|
278
353
|
const out = [];
|
|
279
354
|
out.push(chalk.bold('Output') + chalk.gray(` · pricing ${payload.pricingVersion} · since ${payload.since}`));
|
|
280
|
-
out.push(...headerLines(payload));
|
|
355
|
+
out.push(...headerLines(payload, scenario));
|
|
281
356
|
out.push('');
|
|
282
357
|
if (payload.burn.sessionCount === 0) {
|
|
283
358
|
out.push(chalk.gray('No sessions with cost data found. Run `agents sessions --all` to index, then retry.'));
|
|
284
359
|
console.log(out.join('\n'));
|
|
285
360
|
return;
|
|
286
361
|
}
|
|
287
|
-
out.push(...renderBreakdown(payload.breakdown.rows, payload.breakdown.by));
|
|
362
|
+
out.push(...renderBreakdown(payload.breakdown.rows, payload.breakdown.by, scenario));
|
|
288
363
|
out.push('');
|
|
289
364
|
out.push(chalk.bold('Shipped'));
|
|
290
365
|
out.push(` ${chalk.yellow(String(payload.output.commits))} commits across ${payload.output.reposScanned} repos` + chalk.gray(` (authors: ${payload.output.authors.length > 0 ? payload.output.authors.join(', ') : 'none detected'})`));
|
|
@@ -300,22 +375,22 @@ function renderSingle(payload) {
|
|
|
300
375
|
out.push(chalk.gray(`agent wall-clock: ${formatDuration(payload.burn.durationMs)}`));
|
|
301
376
|
console.log(out.join('\n'));
|
|
302
377
|
}
|
|
303
|
-
function renderFleet(machines, options) {
|
|
378
|
+
function renderFleet(machines, options, scenario) {
|
|
304
379
|
const merged = mergeMachines(machines, options);
|
|
305
380
|
const out = [];
|
|
306
381
|
out.push(chalk.bold('Output') + chalk.gray(` · fleet (${machines.length} machines) · pricing ${merged.pricingVersion} · since ${merged.since}`));
|
|
307
|
-
out.push(...headerLines(merged));
|
|
382
|
+
out.push(...headerLines(merged, scenario));
|
|
308
383
|
out.push('');
|
|
309
384
|
// By machine.
|
|
310
385
|
out.push(chalk.bold('By machine'));
|
|
311
386
|
const nameW = Math.max(...machines.map(m => m.machine.length), 7);
|
|
312
|
-
const burnW = Math.max(...machines.map(m => formatUsd(m.burn
|
|
387
|
+
const burnW = Math.max(...machines.map(m => formatUsd(scenarioCost(m.burn, scenario)).length), 4);
|
|
313
388
|
for (const m of machines) {
|
|
314
389
|
const note = m.error ? chalk.red(` (${m.error})`) : '';
|
|
315
390
|
out.push(' ' +
|
|
316
391
|
padToWidth(m.machine, nameW) +
|
|
317
392
|
' ' +
|
|
318
|
-
chalk.green(padToWidth(formatUsd(m.burn
|
|
393
|
+
chalk.green(padToWidth(formatUsd(scenarioCost(m.burn, scenario)), burnW)) +
|
|
319
394
|
' ' +
|
|
320
395
|
chalk.cyan(padToWidth(formatCompact(m.burn.outputTokens), 7)) +
|
|
321
396
|
' ' +
|
|
@@ -323,7 +398,7 @@ function renderFleet(machines, options) {
|
|
|
323
398
|
note);
|
|
324
399
|
}
|
|
325
400
|
out.push('');
|
|
326
|
-
out.push(...renderBreakdown(merged.breakdown.rows, merged.breakdown.by));
|
|
401
|
+
out.push(...renderBreakdown(merged.breakdown.rows, merged.breakdown.by, scenario));
|
|
327
402
|
out.push('');
|
|
328
403
|
out.push(chalk.gray(notCountedLine(merged.uncostedAgents)));
|
|
329
404
|
console.log(out.join('\n'));
|
package/dist/commands/perf.d.ts
CHANGED
|
@@ -23,6 +23,16 @@ interface PerfGlobalOpts {
|
|
|
23
23
|
}
|
|
24
24
|
/** Map warehouse rows shaped like hook.fire into the existing HookProfileRow UI. */
|
|
25
25
|
export declare function asHookRows(rows: PerfAggregateRow[]): HookProfileRow[];
|
|
26
|
+
/**
|
|
27
|
+
* `err:12% block:40% to:4%` when any rate is present, else ''.
|
|
28
|
+
* Exit 2 (intentional deny) is `block:`, not `err:` — so a deny-by-design
|
|
29
|
+
* guard no longer reads as a crashing hook in the table.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatRateColumn(r: {
|
|
32
|
+
errorRate?: number;
|
|
33
|
+
blockRate?: number;
|
|
34
|
+
timeoutRate?: number;
|
|
35
|
+
}): string;
|
|
26
36
|
export declare function renderHookTable(rows: HookProfileRow[], warnMs: number): void;
|
|
27
37
|
/**
|
|
28
38
|
* Prefer SQLite samples; fall back to the legacy daily JSONL so existing
|
package/dist/commands/perf.js
CHANGED
|
@@ -43,6 +43,8 @@ export function asHookRows(rows) {
|
|
|
43
43
|
cacheMissPct: r.cacheMissPct ?? 0,
|
|
44
44
|
errorCount: r.errorCount ?? 0,
|
|
45
45
|
errorRate: r.errorRate,
|
|
46
|
+
blockCount: r.blockCount ?? 0,
|
|
47
|
+
blockRate: r.blockRate,
|
|
46
48
|
timeoutRate: r.timeoutRate,
|
|
47
49
|
project: r.project,
|
|
48
50
|
}));
|
|
@@ -57,11 +59,17 @@ function printTable(headers, widths, lines, slowFlags) {
|
|
|
57
59
|
console.log(slowFlags[i] ? chalk.yellow(line) : line);
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
+
/**
|
|
63
|
+
* `err:12% block:40% to:4%` when any rate is present, else ''.
|
|
64
|
+
* Exit 2 (intentional deny) is `block:`, not `err:` — so a deny-by-design
|
|
65
|
+
* guard no longer reads as a crashing hook in the table.
|
|
66
|
+
*/
|
|
67
|
+
export function formatRateColumn(r) {
|
|
62
68
|
const parts = [];
|
|
63
69
|
if (r.errorRate)
|
|
64
70
|
parts.push(`err:${Math.round(r.errorRate * 100)}%`);
|
|
71
|
+
if (r.blockRate)
|
|
72
|
+
parts.push(`block:${Math.round(r.blockRate * 100)}%`);
|
|
65
73
|
if (r.timeoutRate)
|
|
66
74
|
parts.push(`to:${Math.round(r.timeoutRate * 100)}%`);
|
|
67
75
|
return parts.join(' ');
|
|
@@ -73,7 +81,7 @@ export function renderHookTable(rows, warnMs) {
|
|
|
73
81
|
console.log(chalk.gray('Hooks write via cache/matches shims into the spool; run a session or resync hooks.'));
|
|
74
82
|
return;
|
|
75
83
|
}
|
|
76
|
-
const widths = { hook: 36, n: 5, p50: 7, p95: 7, p99: 7, mean: 7, max: 7, cache: 22, rate:
|
|
84
|
+
const widths = { hook: 36, n: 5, p50: 7, p95: 7, p99: 7, mean: 7, max: 7, cache: 22, rate: 22 };
|
|
77
85
|
const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
|
|
78
86
|
const header = [
|
|
79
87
|
pad('HOOK', widths.hook),
|
|
@@ -84,7 +92,7 @@ export function renderHookTable(rows, warnMs) {
|
|
|
84
92
|
pad('MEAN', widths.mean),
|
|
85
93
|
pad('MAX', widths.max),
|
|
86
94
|
pad('CACHE', widths.cache),
|
|
87
|
-
pad('ERR/
|
|
95
|
+
pad('ERR/BLOCK/TO', widths.rate),
|
|
88
96
|
].join(' ');
|
|
89
97
|
console.log(chalk.bold(header));
|
|
90
98
|
console.log(chalk.gray('─'.repeat(header.length)));
|
|
@@ -112,8 +120,8 @@ function renderLabelTable(title, rows, warnMs, limit) {
|
|
|
112
120
|
console.log(chalk.gray(`No ${title} samples yet.`));
|
|
113
121
|
return;
|
|
114
122
|
}
|
|
115
|
-
const widths = [40, 5, 7, 7, 7, 7, 7,
|
|
116
|
-
printTable(['LABEL', 'N', 'P50', 'P95', 'P99', 'MEAN', 'MAX', 'ERR/
|
|
123
|
+
const widths = [40, 5, 7, 7, 7, 7, 7, 22];
|
|
124
|
+
printTable(['LABEL', 'N', 'P50', 'P95', 'P99', 'MEAN', 'MAX', 'ERR/BLOCK/TO'], widths, sliced.map((r) => [
|
|
117
125
|
r.label,
|
|
118
126
|
String(r.n),
|
|
119
127
|
formatMs(r.p50Ms),
|
|
@@ -20,9 +20,9 @@ import { machineId } from '../lib/machine-id.js';
|
|
|
20
20
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
21
21
|
import { gatherRemoteActive } from '../lib/session/remote-active.js';
|
|
22
22
|
import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
23
|
-
import { formatFleetWorkspaces, parseRemoteProbe, probeProjectWorkspaces, workspaceTargetsForDef, workspaceWarnings, } from '../lib/project-probe.js';
|
|
23
|
+
import { formatFleetSummary, formatFleetWorkspaces, parseRemoteProbe, probeProjectWorkspaces, workspaceTargetsForDef, workspaceWarnings, } from '../lib/project-probe.js';
|
|
24
24
|
import { listProjectDefs, loadProjectDef, writeProjectDef, removeProjectDef, projectDefPath, isSafeProjectName, validateProjectDef, } from '../lib/projects.js';
|
|
25
|
-
import { rollupSessionsByProject, withDefaultMachine, isDeadStatus, liveDeadSplit, enrichProjectSignals, formatProjectMembersByHost, formatProjectWarnings, } from '../lib/project-status.js';
|
|
25
|
+
import { rollupSessionsByProject, withDefaultMachine, isDeadStatus, formatDeadSummary, liveDeadSplit, enrichProjectSignals, formatProjectMembersByHost, formatProjectWarnings, } from '../lib/project-status.js';
|
|
26
26
|
import { fetchLinearProjectCounts } from '../lib/linear-project-counts.js';
|
|
27
27
|
import { listLinearProjects, pickLinearProject } from '../lib/linear-projects.js';
|
|
28
28
|
import { checkRepoSlug } from '../lib/project-doctor.js';
|
|
@@ -302,8 +302,7 @@ warnWorkspaces = []) {
|
|
|
302
302
|
if (split.dead > 0) {
|
|
303
303
|
// Wreckage is worth a number of its own — 19 crashed sessions is a thing to
|
|
304
304
|
// go fix, not a throughput signal to fold into the headline.
|
|
305
|
-
|
|
306
|
-
console.log(` ${chalk.dim('dead')} ${chalk.yellow(`${split.dead} finished or lost`)} ${chalk.dim(`(${deadDetail})`)}`);
|
|
305
|
+
console.log(` ${chalk.dim('dead')} ${formatDeadSummary(split)}`);
|
|
307
306
|
}
|
|
308
307
|
// Live only, grouped by host when machine stamps exist so "who is on which
|
|
309
308
|
// box" is visible. Flat collapse hid that when harness×status matched across hosts.
|
|
@@ -327,7 +326,8 @@ warnWorkspaces = []) {
|
|
|
327
326
|
if (ships.length)
|
|
328
327
|
console.log(` ${chalk.dim('ships')} ${ships.join(' · ')}`);
|
|
329
328
|
if (linear) {
|
|
330
|
-
|
|
329
|
+
const pct = linear.total > 0 ? ` ${chalk.dim(`(${Math.round((linear.done / linear.total) * 100)}%)`)}` : '';
|
|
330
|
+
console.log(` ${chalk.dim('linear')} ${linear.done}/${linear.total}${linear.truncated ? '+' : ''} done${pct} · ${linear.inProgress} in progress`);
|
|
331
331
|
}
|
|
332
332
|
for (const line of formatMilestoneLines(linear?.milestones ?? [], linear?.nextMilestone, nowMs, milestoneLimit)) {
|
|
333
333
|
console.log(line);
|
|
@@ -346,13 +346,18 @@ warnWorkspaces = []) {
|
|
|
346
346
|
console.log(` ${chalk.dim('tickets')} ${r.tickets.slice(0, 8).join(' · ')}${r.tickets.length > 8 ? ' …' : ''}`);
|
|
347
347
|
}
|
|
348
348
|
if (fleet) {
|
|
349
|
-
const
|
|
350
|
-
if (
|
|
349
|
+
const table = formatFleetWorkspaces(fleet);
|
|
350
|
+
if (table.length === 0) {
|
|
351
351
|
console.log(` ${chalk.dim('fleet')} ${chalk.gray('no workspace paths (set root or repos[].path)')}`);
|
|
352
352
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
353
|
+
else {
|
|
354
|
+
// A compact health line first (scan without reading every host), then the
|
|
355
|
+
// full per-host table under it. The grouped footer carries the actionable
|
|
356
|
+
// subset; this line + the table carry the whole picture.
|
|
357
|
+
[formatFleetSummary(fleet), ...table].forEach((line, i) => {
|
|
358
|
+
console.log(` ${chalk.dim((i === 0 ? 'fleet' : '').padEnd(5))} ${line}`);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
356
361
|
}
|
|
357
362
|
if (remote?.artifacts) {
|
|
358
363
|
const last = remote.lastArtifact ? ` ${chalk.dim(`· last: ${remote.lastArtifact}`)}` : '';
|
|
@@ -639,6 +644,15 @@ export function registerProjectsCommands(program) {
|
|
|
639
644
|
}
|
|
640
645
|
// Compact rollup shows the next milestone; `view` shows every declared one.
|
|
641
646
|
const milestoneLimit = detail ? Number.POSITIVE_INFINITY : 1;
|
|
647
|
+
// Stamp the fleet-wide rollup with when it was taken, so a scrollback isn't
|
|
648
|
+
// mistaken for a live snapshot. A single named `view` skips it (one card, and
|
|
649
|
+
// its own detail makes the freshness obvious).
|
|
650
|
+
if (!detail) {
|
|
651
|
+
const t = new Date(nowMs);
|
|
652
|
+
const hhmm = `${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}`;
|
|
653
|
+
console.log(chalk.dim(`fleet snapshot · as of ${hhmm}`));
|
|
654
|
+
console.log('');
|
|
655
|
+
}
|
|
642
656
|
for (const d of defs) {
|
|
643
657
|
renderCard(d, roll.get(d.name), remote.get(d.name), fleetFor(d), linear.get(d.name), nowMs, milestoneLimit, focus.get(d.name) ?? [], detail,
|
|
644
658
|
// Always feed workspace rows into the warnings footer so behind/dirty
|
|
@@ -5,7 +5,18 @@ interface ResumeOptions {
|
|
|
5
5
|
headless?: boolean;
|
|
6
6
|
cwd?: string;
|
|
7
7
|
quiet?: boolean;
|
|
8
|
+
/** Run on THIS machine even when the session belongs to a peer (escape hatch). */
|
|
9
|
+
here?: boolean;
|
|
8
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* The argv to re-run this resume on the machine that owns the session.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately carries no "don't route again" FLAG — that rides the
|
|
15
|
+
* {@link RESUME_PINNED_ENV} export instead, so the hop also works against a peer
|
|
16
|
+
* on an older CLI (see the constant's docs). Every argv token here exists in the
|
|
17
|
+
* released surface.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildResumeRemoteArgs(sessionId: string, prompt: string | undefined, options: ResumeOptions): string[];
|
|
9
20
|
/** Translate `agents resume <id>` to the canonical `agents run` surface after
|
|
10
21
|
* resolving the source identity. The run command remains the only executor. */
|
|
11
22
|
export declare function buildResumeRunArgs(session: {
|
package/dist/commands/resume.js
CHANGED
|
@@ -3,6 +3,29 @@ import { spawn } from 'child_process';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { setHelpSections } from '../lib/help.js';
|
|
5
5
|
import { resolveSessionMetadataValue } from './sessions.js';
|
|
6
|
+
import { sessionOwnerDevice, consumeResumePinned, RESUME_PINNED_ENV } from '../lib/session/resume-owner.js';
|
|
7
|
+
/**
|
|
8
|
+
* The argv to re-run this resume on the machine that owns the session.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately carries no "don't route again" FLAG — that rides the
|
|
11
|
+
* {@link RESUME_PINNED_ENV} export instead, so the hop also works against a peer
|
|
12
|
+
* on an older CLI (see the constant's docs). Every argv token here exists in the
|
|
13
|
+
* released surface.
|
|
14
|
+
*/
|
|
15
|
+
export function buildResumeRemoteArgs(sessionId, prompt, options) {
|
|
16
|
+
const args = ['resume', sessionId, ...(prompt === undefined ? [] : [prompt])];
|
|
17
|
+
if (options.mode)
|
|
18
|
+
args.push('--mode', options.mode);
|
|
19
|
+
if (options.interactive)
|
|
20
|
+
args.push('--interactive');
|
|
21
|
+
if (options.headless)
|
|
22
|
+
args.push('--headless');
|
|
23
|
+
if (options.cwd)
|
|
24
|
+
args.push('--cwd', options.cwd);
|
|
25
|
+
if (options.quiet)
|
|
26
|
+
args.push('--quiet');
|
|
27
|
+
return args;
|
|
28
|
+
}
|
|
6
29
|
/** Translate `agents resume <id>` to the canonical `agents run` surface after
|
|
7
30
|
* resolving the source identity. The run command remains the only executor. */
|
|
8
31
|
export function buildResumeRunArgs(session, prompt, options) {
|
|
@@ -29,7 +52,11 @@ export function registerResumeCommand(program) {
|
|
|
29
52
|
.option('--headless', 'Resume headlessly (a prompt is required)')
|
|
30
53
|
.option('--cwd <path>', 'Override the recorded working directory')
|
|
31
54
|
.option('-q, --quiet', 'Suppress routing banners')
|
|
55
|
+
.option('--here', 'Run on this machine even if the session belongs to another device')
|
|
32
56
|
.action(async (sessionId, prompt, options) => {
|
|
57
|
+
// Read (and clear) the routing pin before anything else, so it can never
|
|
58
|
+
// reach the agent's own children.
|
|
59
|
+
const pinnedHere = consumeResumePinned() || !!options.here;
|
|
33
60
|
const outcome = await resolveSessionMetadataValue(sessionId.trim());
|
|
34
61
|
if (outcome.kind === 'partial') {
|
|
35
62
|
console.error(chalk.red(`Could not resolve session while these devices were unavailable: ${outcome.failedPeers.join(', ')}`));
|
|
@@ -46,6 +73,29 @@ export function registerResumeCommand(program) {
|
|
|
46
73
|
process.exitCode = 1;
|
|
47
74
|
return;
|
|
48
75
|
}
|
|
76
|
+
// The harness keeps its conversation state on the machine that produced
|
|
77
|
+
// the session, so a peer-owned session MUST resume there. Running it here
|
|
78
|
+
// starts the agent against state this box does not have — silently, since
|
|
79
|
+
// a synced mirror makes the transcript look local (RUSH-2022).
|
|
80
|
+
const owner = pinnedHere ? undefined : sessionOwnerDevice(outcome.session);
|
|
81
|
+
if (owner) {
|
|
82
|
+
if (!options.quiet) {
|
|
83
|
+
process.stderr.write(chalk.gray(`[agents] session ${outcome.session.shortId} belongs to ${owner} → resuming there\n`));
|
|
84
|
+
}
|
|
85
|
+
// `runOnPeer` is the existing transport for "this session's transcript
|
|
86
|
+
// and agent binary are on that box" (lib/session/remote-list.ts) — the
|
|
87
|
+
// same one the picker already uses. Not the `--host` passthrough: that
|
|
88
|
+
// one re-discovers locally and marks the run AGENTS_FLEET_REMOTE, which
|
|
89
|
+
// a long-lived resumed session must not inherit.
|
|
90
|
+
const { runOnPeer } = await import('../lib/session/remote-list.js');
|
|
91
|
+
const rc = await runOnPeer(buildResumeRemoteArgs(outcome.session.id, prompt, options), owner, { tty: !!process.stdout.isTTY, env: { [RESUME_PINNED_ENV]: '1' } });
|
|
92
|
+
if (rc === 'no-target') {
|
|
93
|
+
console.error(chalk.red(`Session ${outcome.session.shortId} lives on ${owner}, which isn't a reachable device right now.`));
|
|
94
|
+
console.error(chalk.gray(`Register/wake it (agents devices), or run there: agents ssh ${owner}`));
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
49
99
|
const args = buildResumeRunArgs(outcome.session, prompt, options);
|
|
50
100
|
const child = spawn(process.execPath, [process.argv[1], ...args], {
|
|
51
101
|
stdio: 'inherit',
|
|
@@ -82,6 +132,7 @@ export function registerResumeCommand(program) {
|
|
|
82
132
|
agents resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 --mode edit`,
|
|
83
133
|
notes: `
|
|
84
134
|
A full ID resolves from the local session database first (zero SSH) and, on a local miss, fans out with the first peer holding it cancelling the rest. An exact label always consults the fleet (labels are not globally unique) and auto-resumes the one match; a cross-machine label collision surfaces as an ambiguity.
|
|
135
|
+
A session that ran on another device resumes ON that device over SSH — the harness's conversation state lives there, so running it here would start against state this machine does not have. --here overrides that and runs locally.
|
|
85
136
|
Use agents run auto --resume <id> when the original account is unavailable and another harness may continue.`,
|
|
86
137
|
});
|
|
87
138
|
}
|
|
@@ -1560,8 +1560,17 @@ export function registerRoutinesCommands(program) {
|
|
|
1560
1560
|
const unknown = [...selectedSet].filter((device) => !all.includes(device));
|
|
1561
1561
|
if (unknown.length > 0)
|
|
1562
1562
|
throw new Error(`Unknown device${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`);
|
|
1563
|
+
// --set/--clear fan out to every registered device so peers outside the
|
|
1564
|
+
// new set get paused. An asleep/offline peer must not abort the whole
|
|
1565
|
+
// pin: the target may already be applied, and an unreachable box cannot
|
|
1566
|
+
// be running the routine (it picks up the enabled set on next sync).
|
|
1567
|
+
// Exit non-zero only when a *selected* device could not be enabled
|
|
1568
|
+
// (github.com/phnx-labs/agents-cli#2118).
|
|
1569
|
+
const skipped = [];
|
|
1570
|
+
const failedTargets = [];
|
|
1563
1571
|
for (const device of all) {
|
|
1564
1572
|
const action = selectedSet.has(device) ? 'resume' : 'pause';
|
|
1573
|
+
const isTarget = selectedSet.has(device);
|
|
1565
1574
|
if (device === normalizeHost(machineId())) {
|
|
1566
1575
|
setJobEnabled(name, action === 'resume');
|
|
1567
1576
|
if (isDaemonRunning())
|
|
@@ -1570,12 +1579,30 @@ export function registerRoutinesCommands(program) {
|
|
|
1570
1579
|
}
|
|
1571
1580
|
const launch = getCliLaunch(['routines', action, name, '--host', device]);
|
|
1572
1581
|
const result = spawnSync(launch.command, launch.args, { stdio: 'inherit', env: process.env });
|
|
1573
|
-
if ((result.status ?? 1) !== 0)
|
|
1574
|
-
|
|
1582
|
+
if ((result.status ?? 1) !== 0) {
|
|
1583
|
+
if (isTarget) {
|
|
1584
|
+
failedTargets.push(device);
|
|
1585
|
+
}
|
|
1586
|
+
else {
|
|
1587
|
+
skipped.push({ device, action });
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1575
1590
|
}
|
|
1591
|
+
for (const s of skipped) {
|
|
1592
|
+
console.log(chalk.yellow(`Skipped ${s.action} of '${name}' on ${s.device} (unreachable; will sync when online)`));
|
|
1593
|
+
}
|
|
1594
|
+
if (failedTargets.length > 0) {
|
|
1595
|
+
const offlineNote = skipped.length > 0
|
|
1596
|
+
? ` (also skipped offline: ${skipped.map((s) => s.device).join(', ')})`
|
|
1597
|
+
: '';
|
|
1598
|
+
throw new Error(`Could not enable '${name}' on: ${failedTargets.join(', ')}${offlineNote}`);
|
|
1599
|
+
}
|
|
1600
|
+
const offlineNote = skipped.length > 0
|
|
1601
|
+
? ` (${skipped.length} offline device${skipped.length === 1 ? '' : 's'} skipped)`
|
|
1602
|
+
: '';
|
|
1576
1603
|
console.log(chalk.green(selected.length === 0
|
|
1577
|
-
? `Routine '${name}' disabled on every registered device`
|
|
1578
|
-
: `Routine '${name}' enabled on: ${selected.join(', ')}`));
|
|
1604
|
+
? `Routine '${name}' disabled on every registered device${offlineNote}`
|
|
1605
|
+
: `Routine '${name}' enabled on: ${selected.join(', ')}${offlineNote}`));
|
|
1579
1606
|
};
|
|
1580
1607
|
if (options.clear) {
|
|
1581
1608
|
await applyDevices([]);
|
|
@@ -4,11 +4,48 @@ export interface RunAccountChoice {
|
|
|
4
4
|
name: string;
|
|
5
5
|
value: string;
|
|
6
6
|
disabled?: string;
|
|
7
|
+
/** Can serve a run right now: signed in, authenticated, and under quota. */
|
|
7
8
|
ready: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Selectable, but picking it launches the harness so you can authenticate
|
|
11
|
+
* first (RUSH-2334). Mutually exclusive with `ready`; never `disabled`.
|
|
12
|
+
*/
|
|
13
|
+
signInRequired: boolean;
|
|
8
14
|
}
|
|
9
15
|
/** Human-readable remaining capacity for every window the provider exposes. */
|
|
10
16
|
export declare function formatAccountLimits(candidate: RotateCandidate): string;
|
|
11
17
|
/** Build aligned picker rows with usable accounts first and unsafe rows disabled. */
|
|
12
18
|
export declare function buildRunAccountChoices(candidates: RotateCandidate[], globalDefault: string | null): RunAccountChoice[];
|
|
19
|
+
/**
|
|
20
|
+
* Whether a zero-healthy run may recover by launching for a login, or must keep
|
|
21
|
+
* failing loud. Three inputs, all of which have to hold:
|
|
22
|
+
*
|
|
23
|
+
* - `recoverable` — at least one excluded account is only auth-blocked. An
|
|
24
|
+
* all-throttled set is never launched (RUSH-2132): only a window reset clears it.
|
|
25
|
+
* - `tty` — a login needs a human present; off a TTY nobody can complete one.
|
|
26
|
+
* - `json` — `--json` marks a MACHINE consumer, which must never be handed a
|
|
27
|
+
* picker or dropped into a login TUI. This mirrors the canonical
|
|
28
|
+
* `Surface.interactive = tty && !json` in `commands/utils.ts`; a `--json` caller
|
|
29
|
+
* gets the parseable fail-loud error instead.
|
|
30
|
+
*/
|
|
31
|
+
export declare function signInLaunchDecision(input: {
|
|
32
|
+
recoverable: number;
|
|
33
|
+
tty: boolean;
|
|
34
|
+
json: boolean;
|
|
35
|
+
}): 'launch' | 'fail-loud';
|
|
36
|
+
/**
|
|
37
|
+
* Choose which installed version to launch so the user can authenticate, when a
|
|
38
|
+
* strategy found zero healthy accounts but at least one is merely signed out
|
|
39
|
+
* (RUSH-2334). Returns the version to launch, or null if the user cancelled.
|
|
40
|
+
*
|
|
41
|
+
* A single candidate does NOT prompt — a one-item picker is pure noise, and the
|
|
42
|
+
* only thing to decide has one answer. Several candidates fall through to the
|
|
43
|
+
* normal account picker, which shows every account with its state so the choice
|
|
44
|
+
* is informed (throttled rows stay disabled there).
|
|
45
|
+
*
|
|
46
|
+
* Callers MUST have already confirmed an interactive terminal: off a TTY there
|
|
47
|
+
* is nobody to complete the login, and the run should fail loud instead.
|
|
48
|
+
*/
|
|
49
|
+
export declare function pickSignInLaunchVersion(agent: AgentId, recoverable: RotateCandidate[], quiet?: boolean): Promise<string | null>;
|
|
13
50
|
/** Prompt for one safe installed account/version. A cancelled picker launches nothing. */
|
|
14
51
|
export declare function pickRunAccountCandidate(agent: AgentId): Promise<RotateCandidate | null>;
|