@phnx-labs/agents-cli 1.22.104 → 1.22.106
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 +18 -0
- package/README.md +19 -1
- package/dist/browser.js +0 -0
- package/dist/commands/exec.js +135 -233
- package/dist/commands/resume.d.ts +6 -21
- package/dist/commands/resume.js +18 -55
- package/dist/commands/run-device-picker.d.ts +58 -0
- package/dist/commands/run-device-picker.js +222 -0
- package/dist/commands/sessions-resume.d.ts +4 -0
- package/dist/commands/sessions-resume.js +132 -49
- package/dist/commands/sessions.js +32 -5
- package/dist/commands/setup-computer.js +2 -2
- package/dist/index.js +0 -0
- package/dist/lib/accounting/account-launch.d.ts +54 -0
- package/dist/lib/accounting/account-launch.js +117 -0
- package/dist/lib/accounting/account-pool-collect.js +2 -1
- package/dist/lib/accounting/account-pool.d.ts +2 -0
- package/dist/lib/accounting/account-pool.js +1 -0
- package/dist/lib/accounting/rotate.d.ts +32 -6
- package/dist/lib/accounting/rotate.js +70 -42
- package/dist/lib/accounting/usage.d.ts +71 -0
- package/dist/lib/accounting/usage.js +160 -11
- package/dist/lib/exec-account-home.d.ts +3 -1
- package/dist/lib/exec-account-home.js +2 -2
- package/dist/lib/exec.d.ts +27 -1
- package/dist/lib/exec.js +150 -27
- package/dist/lib/models.d.ts +1 -1
- package/dist/lib/models.js +4 -4
- package/dist/lib/session/actor-sidecar.d.ts +3 -11
- package/dist/lib/session/actor-sidecar.js +3 -0
- package/dist/lib/session/claude-accounts.d.ts +12 -73
- package/dist/lib/session/claude-accounts.js +32 -70
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +18 -5
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +116 -15
- package/dist/lib/session/recovery.d.ts +30 -34
- package/dist/lib/session/recovery.js +212 -76
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/teams/placement-probe.js +1 -1
- package/dist/session-tracker/dist/adapters/claude.d.ts +10 -0
- package/dist/session-tracker/dist/adapters/claude.js +45 -0
- package/dist/session-tracker/dist/hook.sh +191 -0
- package/dist/session-tracker/dist/index.d.ts +19 -0
- package/dist/session-tracker/dist/index.js +67 -0
- package/dist/session-tracker/dist/install-hook.d.ts +19 -0
- package/dist/session-tracker/dist/install-hook.js +245 -0
- package/dist/session-tracker/dist/prune-state.d.ts +2 -0
- package/dist/session-tracker/dist/prune-state.js +7 -0
- package/dist/session-tracker/dist/reader.d.ts +7 -0
- package/dist/session-tracker/dist/reader.js +151 -0
- package/dist/session-tracker/dist/state-file.d.ts +10 -0
- package/dist/session-tracker/dist/state-file.js +119 -0
- package/dist/session-tracker/dist/types.d.ts +32 -0
- package/dist/session-tracker/dist/types.js +1 -0
- package/dist/session-tracker/dist/writer.d.ts +12 -0
- package/dist/session-tracker/dist/writer.js +27 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.106
|
|
4
|
+
|
|
5
|
+
- Fixed release packages omitting the session-tracking hook. Release qualification now uses the complete CLI build, so installed agents can record the account that owns each native session.
|
|
6
|
+
|
|
7
|
+
- **Resume conversations by account with one shared picker (PHNX-3940).** `ag run claude --resume` opens the same history picker as `agents sessions resume --agent claude`; `claude#work` applies `--account work`. Selected conversations retain their account, model, permissions, cwd and run options while using the installed binary. Canonical account homes and short Codex homes are discovered independently of version installs. Explicit terminal backends work with session IDs. Native context is verified after cold discovery, and archived replay requires an explicit choice. Origin account identity survives alias updates and index rescans; a model refusal does not disable unrelated models or accounts.
|
|
8
|
+
|
|
9
|
+
## 1.22.105
|
|
10
|
+
|
|
11
|
+
- Computer setup installs standalone Computer CLI 0.1.2, whose application code is obfuscated before compilation to increase inspection effort. Agents continues to provide permissions and session tracking through its adapter.
|
|
12
|
+
|
|
13
|
+
- Fleet test runs use separate temporary workspaces and clean up their own copies. Router account fixtures run only with isolated file-backed secret storage, preserving native keyrings.
|
|
14
|
+
|
|
15
|
+
- **Device picker module for `agents run <agent>@` (PHNX-4083).** New
|
|
16
|
+
`run-device-picker` command module builds the fleet device menu entirely from
|
|
17
|
+
on-disk state — the device registry, the cached fleet stats, configured roles
|
|
18
|
+
and descriptions, and the fleet-synced account catalog — with zero SSH, zero
|
|
19
|
+
network, and no re-probing of stale rows. Source: `src/commands/run-device-picker.ts`.
|
|
20
|
+
|
|
3
21
|
## 1.22.104
|
|
4
22
|
|
|
5
23
|
- **Interactive Claude runs on a worker fail loud instead of dropping to the login screen (PHNX-3502 sibling).** `agents run claude --interactive --device auto/<worker>` whose selected account had no synced setup-token used to fall through to Claude Code's "Select login method" screen — an interactive OAuth on a headless box that never persists, so Anthropic re-prompted on the next run (the repeated re-login loop). The run is now refused before spawn with the real fix (pin a live account with `agents run claude#<name>`, set the fleet default with `agents accounts default claude <name>`, or mint the account's token on a headed box with `agents accounts login claude#<name>`). Headless runs are unchanged (they already fail loud with a 401), and headed boxes still show their normal native-login prompt. Source: `cli/src/lib/harness/adapters/claude.ts`, `cli/src/lib/exec.ts`.
|
package/README.md
CHANGED
|
@@ -431,6 +431,24 @@ Both targets are on-demand backups only; neither enables background session sync
|
|
|
431
431
|
|
|
432
432
|
### Resume anywhere — and stay resumed
|
|
433
433
|
|
|
434
|
+
Omit the session ID to choose from an interactive history picker with descriptions
|
|
435
|
+
and conversation previews. These commands use the same picker and account filter:
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
ag run claude --resume
|
|
439
|
+
agents sessions resume --agent claude
|
|
440
|
+
|
|
441
|
+
ag run claude#work --resume
|
|
442
|
+
agents sessions resume --agent claude --account work
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Resume restores the conversation's account, model, mode and working directory with
|
|
446
|
+
the currently installed harness. Updating that binary does not select a different
|
|
447
|
+
account. `agents sessions resume <id> --vscodium` opens the selected conversation in
|
|
448
|
+
VSCodium. If only archived context remains, the CLI asks before starting a new
|
|
449
|
+
conversation from it; a missing transcript fails before launching an agent.
|
|
450
|
+
|
|
451
|
+
|
|
434
452
|
Pick up any past conversation and drop it back into a terminal:
|
|
435
453
|
|
|
436
454
|
```bash
|
|
@@ -446,7 +464,7 @@ agents run auto --resume 019fd0c8-b3e9-77a2-a1a4-444698c4d897 # adapt if its ac
|
|
|
446
464
|
|
|
447
465
|
### Send an agent to the background — and bring it back
|
|
448
466
|
|
|
449
|
-
Running 30 agents and drowning in terminal tabs? `agents sessions detach <id>` stops a session's interactive process and keeps it working **headless** in the background -- it drives its task to done unattended, no tab, lower cost. `agents sessions resume <id>` brings it back through the same origin-device recovery decision: native resume in the
|
|
467
|
+
Running 30 agents and drowning in terminal tabs? `agents sessions detach <id>` stops a session's interactive process and keeps it working **headless** in the background -- it drives its task to done unattended, no tab, lower cost. `agents sessions resume <id>` brings it back through the same origin-device recovery decision: native resume in the owned account home, or an explicitly chosen same-harness `/continue` replay when native context is unavailable.
|
|
450
468
|
|
|
451
469
|
```
|
|
452
470
|
agents sessions detach a1b2c3d4 # go headless in the background, keep working
|
package/dist/browser.js
CHANGED
|
File without changes
|
package/dist/commands/exec.js
CHANGED
|
@@ -521,7 +521,7 @@ export function registerRunCommand(program) {
|
|
|
521
521
|
.option('--list-tasks', 'With --broadcast: list available broadcast task ids')
|
|
522
522
|
.option('--results [run-id]', 'With --broadcast: show one saved matrix run, or list saved runs newest first')
|
|
523
523
|
.option('--concurrency <n>', 'With --broadcast: maximum cells running at once', '3')
|
|
524
|
-
.option('--resume [id]', '
|
|
524
|
+
.option('--resume [id]', 'Resume a conversation with its account on the origin device. Omit the id for the shared session picker; #account filters the history. Pair an id with a prompt to continue headlessly.')
|
|
525
525
|
.option('--session-id <id>', 'Force a NEW conversation to use this exact session UUID (Claude only). This CREATES a session — to resume an existing one, use --resume.', parseExplicitSessionId)
|
|
526
526
|
.option('--name <slug>', 'Name the run — seeds the session label so it shows up as `<name>` in `agents sessions` and resolves by it (and `agents hosts logs <name>` for --device runs) instead of an opaque id. An agent-generated title later refines the label; your name shows until then. Optional.')
|
|
527
527
|
.option('--notify', 'Post a desktop notification when a headless run finishes. Fired by this process on exit, so it survives whatever launched the run (the menu bar dispatching it, a terminal you closed).')
|
|
@@ -692,7 +692,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
692
692
|
given. --cloud is mutually exclusive with --device/--lease and with
|
|
693
693
|
local-run flags (--loop, --resume, --secrets, --terminal, …).
|
|
694
694
|
|
|
695
|
-
Resume: --resume <id> resolves full IDs locally first, then fleet-wide, and recovers on the source device with its cwd/mode.
|
|
695
|
+
Resume: --resume <id> resolves full IDs locally first, then fleet-wide, and recovers on the source device with its cwd/mode. Resume preserves the conversation account and uses the installed binary; starting a new conversation from archived context requires an explicit choice. agents sessions resume <id> infers the harness too.
|
|
696
696
|
|
|
697
697
|
Passthrough: everything after -- is forwarded verbatim to the underlying agent CLI.
|
|
698
698
|
agents run kimi -- --plan --some-native-flag value
|
|
@@ -861,6 +861,27 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
861
861
|
if (specAccountLabel)
|
|
862
862
|
options.account = specAccountLabel;
|
|
863
863
|
}
|
|
864
|
+
// Bare resume is a session selection request, before fresh-run defaults or placement.
|
|
865
|
+
// Concrete IDs re-enter the executor below, so this cannot recurse.
|
|
866
|
+
if (options.resume === true || options.resume === '') {
|
|
867
|
+
if (options.sessionId || options.loop || options.fallback || options.resumeCheckpoint || options.lease) {
|
|
868
|
+
throw new Error('--resume cannot be combined with --session-id, --loop, --fallback, --resume-checkpoint, or --lease.');
|
|
869
|
+
}
|
|
870
|
+
const { sessionsResumeAction } = await import('./sessions-resume.js');
|
|
871
|
+
await sessionsResumeAction(undefined, prompt, {
|
|
872
|
+
agent: normalizedAgentSpec.split('#')[0] === RUN_AUTO_KEYWORD ? undefined : normalizedAgentSpec.split('#')[0],
|
|
873
|
+
account: options.account,
|
|
874
|
+
model: options.model,
|
|
875
|
+
mode: command.getOptionValueSource('mode') === 'default' ? undefined : options.mode,
|
|
876
|
+
interactive: options.interactive,
|
|
877
|
+
headless: options.headless,
|
|
878
|
+
cwd: options.cwd,
|
|
879
|
+
quiet: options.quiet,
|
|
880
|
+
device: options.host || options.device || options.on || options.computer,
|
|
881
|
+
runArgs: rawArgs.slice(2),
|
|
882
|
+
});
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
864
885
|
// Hard-deprecated harnesses cannot be run — point the user at the successor.
|
|
865
886
|
const runBaseAgentName = normalizedAgentSpec.split('#')[0].split('@')[0];
|
|
866
887
|
const runBaseAgentId = resolveAgentName(runBaseAgentName);
|
|
@@ -908,7 +929,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
908
929
|
delete process.env.AGENTS_RESUME_SOURCE_JSON;
|
|
909
930
|
const outcome = injectedSource
|
|
910
931
|
? { kind: 'resolved', session: injectedSource }
|
|
911
|
-
: await (await import('./sessions.js')).resolveSessionMetadataValue(selector);
|
|
932
|
+
: await (await import('./sessions.js')).resolveSessionMetadataValue(selector, { agent: runBaseAgentId ?? undefined });
|
|
912
933
|
if (outcome.kind === 'partial') {
|
|
913
934
|
// RUSH-2492: an unreachable peer is a warning, not a hard failure. The
|
|
914
935
|
// resolver already resolves an id found on the reachable fleet (SES-9a),
|
|
@@ -929,17 +950,12 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
929
950
|
process.exit(1);
|
|
930
951
|
}
|
|
931
952
|
resolvedResumeSource = outcome.session;
|
|
932
|
-
const [requestedAgent
|
|
953
|
+
const [requestedAgent] = normalizedAgentSpec.split('#')[0].split('@');
|
|
933
954
|
if (!autoHarnessRequested && requestedAgent !== resolvedResumeSource.agent) {
|
|
934
955
|
console.error(chalk.red(`Session ${resolvedResumeSource.shortId} belongs to ${resolvedResumeSource.agent}, not ${requestedAgent}. ` +
|
|
935
956
|
`Use: agents sessions resume ${resolvedResumeSource.id}`));
|
|
936
957
|
process.exit(1);
|
|
937
958
|
}
|
|
938
|
-
if (!autoHarnessRequested && requestedVersion && requestedVersion !== resolvedResumeSource.version) {
|
|
939
|
-
console.error(chalk.red(`Session ${resolvedResumeSource.shortId} started with ${resolvedResumeSource.agent}@${resolvedResumeSource.version ?? 'unknown'}, ` +
|
|
940
|
-
`not @${requestedVersion}.`));
|
|
941
|
-
process.exit(1);
|
|
942
|
-
}
|
|
943
959
|
// Omitted --mode inherits the effective source mode. Commander keeps
|
|
944
960
|
// the normal new-run default as a real value, so consult its provenance
|
|
945
961
|
// rather than mistaking the default for an explicit override.
|
|
@@ -960,7 +976,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
960
976
|
options.host = sourcePeer;
|
|
961
977
|
}
|
|
962
978
|
else if (sourcePeer && explicitPlacement && !hostTargetGiven(options).some((host) => sessionRecoveryDestinationMatches(resolvedResumeSource, host))) {
|
|
963
|
-
console.error(chalk.red(`Session ${resolvedResumeSource.shortId} must recover on ${sourcePeer}, where its
|
|
979
|
+
console.error(chalk.red(`Session ${resolvedResumeSource.shortId} must recover on ${sourcePeer}, where its conversation state is stored; ` +
|
|
964
980
|
`the requested device was ${hostTargetGiven(options).join(', ')}.`));
|
|
965
981
|
process.exit(1);
|
|
966
982
|
}
|
|
@@ -972,7 +988,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
972
988
|
if (!sourcePeer) {
|
|
973
989
|
try {
|
|
974
990
|
const { resolveSessionRecovery } = await import('../lib/session/recovery.js');
|
|
975
|
-
resolvedRecoveryTarget = await resolveSessionRecovery(resolvedResumeSource);
|
|
991
|
+
resolvedRecoveryTarget = await resolveSessionRecovery(resolvedResumeSource, undefined, { account: options.account, model: options.model ?? resolvedResumeSource.model });
|
|
976
992
|
}
|
|
977
993
|
catch (err) {
|
|
978
994
|
console.error(chalk.red(err.message));
|
|
@@ -981,7 +997,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
981
997
|
normalizedAgentSpec = `${resolvedRecoveryTarget.agent}@${resolvedRecoveryTarget.version}`;
|
|
982
998
|
autoHarnessRequested = false;
|
|
983
999
|
if (!options.quiet)
|
|
984
|
-
process.stderr.write(chalk.gray(`[agents] session recovery → ${resolvedRecoveryTarget.mode} ${
|
|
1000
|
+
process.stderr.write(chalk.gray(`[agents] session recovery → ${resolvedRecoveryTarget.mode} ${resolvedRecoveryTarget.agent} on ${sourceMachine ?? machineId()} · ${resolvedRecoveryTarget.reason}\n`));
|
|
985
1001
|
}
|
|
986
1002
|
else if (autoHarnessRequested) {
|
|
987
1003
|
normalizedAgentSpec = sourceAgent;
|
|
@@ -1419,8 +1435,6 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1419
1435
|
hostRejects.push(RUN_OPTION_REJECT_MESSAGES.allowExpired);
|
|
1420
1436
|
if (options.resumeCheckpoint)
|
|
1421
1437
|
hostRejects.push(RUN_OPTION_REJECT_MESSAGES.resumeCheckpoint);
|
|
1422
|
-
if (options.resume === true)
|
|
1423
|
-
hostRejects.push(RUN_OPTION_REJECT_MESSAGES.resumeBare);
|
|
1424
1438
|
if (hostRejects.length > 0) {
|
|
1425
1439
|
for (const msg of hostRejects)
|
|
1426
1440
|
console.error(chalk.red(msg));
|
|
@@ -1564,7 +1578,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1564
1578
|
version: resumeId ? undefined : runVersion,
|
|
1565
1579
|
accountPicker: accountPickerRequested,
|
|
1566
1580
|
strategy: resumeId ? undefined : runStrategy,
|
|
1567
|
-
account:
|
|
1581
|
+
account: options.account,
|
|
1568
1582
|
fallback: options.fallback,
|
|
1569
1583
|
prompt,
|
|
1570
1584
|
mode: forwardedMode,
|
|
@@ -1672,7 +1686,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1672
1686
|
agent: runAgent,
|
|
1673
1687
|
version: resumeId ? undefined : runVersion,
|
|
1674
1688
|
strategy: resumeId ? undefined : runStrategy,
|
|
1675
|
-
account:
|
|
1689
|
+
account: options.account,
|
|
1676
1690
|
fallback: options.fallback,
|
|
1677
1691
|
prompt,
|
|
1678
1692
|
mode: forwardedMode,
|
|
@@ -2207,87 +2221,95 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2207
2221
|
}
|
|
2208
2222
|
}
|
|
2209
2223
|
version = resolveVersionAlias(agent, version);
|
|
2210
|
-
// Account selection follows the binding order: explicit --account → exact
|
|
2211
|
-
// `agent@version` binding → device-scoped `agent` binding → per-harness
|
|
2212
|
-
// default. The exact-installation binding needs the concrete launch
|
|
2213
|
-
// version, so resolve it (default when the run named no version).
|
|
2214
2224
|
const { resolveSpawnAccount } = await import('../lib/account-registry.js');
|
|
2215
|
-
// Binding key: a custom harness keys on its own profile name; a native /
|
|
2216
|
-
// global run keys on the exact `agent@version` (default when unpinned).
|
|
2217
|
-
const bindingTarget = fromProfile ? rawAgent : (version ? `${agent}@${version}` : `${agent}@${getGlobalDefault(agent) ?? ''}`);
|
|
2218
2225
|
let spawnAccount = null;
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2226
|
+
let launchAccount;
|
|
2227
|
+
if (resolvedRecoveryTarget) {
|
|
2228
|
+
const { resolveLocalAccountLaunch } = await import('../lib/accounting/account-launch.js');
|
|
2229
|
+
launchAccount = await resolveLocalAccountLaunch({
|
|
2230
|
+
agent,
|
|
2231
|
+
executableVersion: resolvedRecoveryTarget.version,
|
|
2232
|
+
candidate: resolvedRecoveryTarget.candidate,
|
|
2233
|
+
useDefault: false,
|
|
2234
|
+
});
|
|
2235
|
+
execHome = resolvedRecoveryTarget.mode === 'native' ? (resolvedRecoveryTarget.execHome ?? launchAccount.execHome) : launchAccount.execHome;
|
|
2236
|
+
accountConfigVersion = resolvedRecoveryTarget.mode === 'native' ? (resolvedRecoveryTarget.configVersion ?? launchAccount.configVersion) : launchAccount.configVersion;
|
|
2237
|
+
accountEnv = { ...accountEnv, ...launchAccount.env };
|
|
2225
2238
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2239
|
+
else {
|
|
2240
|
+
// Binding key: a custom harness keys on its own profile name; a native /
|
|
2241
|
+
// global run keys on the exact `agent@version` (default when unpinned).
|
|
2242
|
+
const bindingTarget = fromProfile ? rawAgent : (version ? `${agent}@${version}` : `${agent}@${getGlobalDefault(agent) ?? ''}`);
|
|
2243
|
+
try {
|
|
2244
|
+
spawnAccount = resolveSpawnAccount(options.account, agent, version, readMeta(), { useDefault: !fromProfile, provider: profileProvider, target: bindingTarget });
|
|
2245
|
+
}
|
|
2246
|
+
catch (err) {
|
|
2247
|
+
console.error(chalk.red(err.message));
|
|
2231
2248
|
process.exit(1);
|
|
2232
2249
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2250
|
+
// Downstream rotation gating asks only "was an account selected?".
|
|
2251
|
+
if (spawnAccount) {
|
|
2252
|
+
if (options.cloud || options.provider || options.lease) {
|
|
2253
|
+
console.error(chalk.red('--account selects a device-local credential and cannot be combined with cloud or lease placement.'));
|
|
2254
|
+
process.exit(1);
|
|
2255
|
+
}
|
|
2256
|
+
if (spawnAccount.kind === 'native') {
|
|
2257
|
+
// `#name` / `--account` on a --device run is forwarded unchanged at
|
|
2258
|
+
// dispatch time; the peer resolves ITS slot. This block is local-only.
|
|
2259
|
+
const remoteTarget = options.host || options.device;
|
|
2260
|
+
if (!remoteTarget) {
|
|
2261
|
+
const { adoptedConfigPointsAtHome, adoptedSymlinkMismatchError, durableSlotEnv, isSymlinkAdoptedHarness, resolveNativeSpawnHome, symlinkAdoptedAccountError, } = await import('../lib/exec-account-home.js');
|
|
2262
|
+
const { readMeta } = await import('../lib/state.js');
|
|
2263
|
+
const meta = readMeta();
|
|
2264
|
+
if (isSymlinkAdoptedHarness(agent)) {
|
|
2265
|
+
const defaultName = meta.accounts?.defaults?.[agent];
|
|
2266
|
+
if (spawnAccount.name !== defaultName) {
|
|
2267
|
+
console.error(chalk.red(symlinkAdoptedAccountError(agent, spawnAccount.name, defaultName)));
|
|
2268
|
+
process.exit(1);
|
|
2269
|
+
}
|
|
2246
2270
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2271
|
+
try {
|
|
2272
|
+
const resolved = await resolveNativeSpawnHome(agent, spawnAccount, meta);
|
|
2273
|
+
if (isSymlinkAdoptedHarness(agent) && !adoptedConfigPointsAtHome(agent, resolved.execHome)) {
|
|
2274
|
+
console.error(chalk.red(adoptedSymlinkMismatchError(agent, spawnAccount.name, resolved.execHome)));
|
|
2275
|
+
process.exit(1);
|
|
2276
|
+
}
|
|
2277
|
+
execHome = resolved.execHome;
|
|
2278
|
+
// A worker's durable api-key slot holds no file: the pushed key
|
|
2279
|
+
// rides the harness env var (CURSOR_API_KEY, …) on this launch.
|
|
2280
|
+
const durable = durableSlotEnv(agent, spawnAccount, resolved, meta);
|
|
2281
|
+
if (Object.keys(durable).length > 0)
|
|
2282
|
+
accountEnv = { ...accountEnv, ...durable };
|
|
2283
|
+
if (resolved.source === 'legacy-home') {
|
|
2284
|
+
accountConfigVersion = resolved.label;
|
|
2285
|
+
// A leftover version-labeled home is both the spawn HOME and
|
|
2286
|
+
// the account's installation — the pre-T5 `resolveAccountVersion`
|
|
2287
|
+
// behavior. An explicit `@<label>` still controls the binary.
|
|
2288
|
+
if (!version && !fromProfile && resolved.label)
|
|
2289
|
+
version = resolved.label;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
catch (err) {
|
|
2293
|
+
console.error(chalk.red(err.message));
|
|
2252
2294
|
process.exit(1);
|
|
2253
2295
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
accountEnv = { ...accountEnv, ...durable };
|
|
2260
|
-
if (resolved.source === 'legacy-home') {
|
|
2261
|
-
accountConfigVersion = resolved.label;
|
|
2262
|
-
// A leftover version-labeled home is both the spawn HOME and
|
|
2263
|
-
// the account's installation — the pre-T5 `resolveAccountVersion`
|
|
2264
|
-
// behavior. An explicit `@<label>` still controls the binary.
|
|
2265
|
-
if (!version && !fromProfile && resolved.label)
|
|
2266
|
-
version = resolved.label;
|
|
2296
|
+
// Slot / provisioned: binary comes from the one managed install unless `@<label>` pins it.
|
|
2297
|
+
if (!version && !fromProfile) {
|
|
2298
|
+
const { ensureHarnessInstallation } = await import('../lib/installations/store.js');
|
|
2299
|
+
const { installation } = await ensureHarnessInstallation(agent);
|
|
2300
|
+
version = installation.label;
|
|
2267
2301
|
}
|
|
2302
|
+
if (!options.quiet)
|
|
2303
|
+
process.stderr.write(chalk.gray(`[agents] account '${spawnAccount.name}' · ${agent}\n`));
|
|
2268
2304
|
}
|
|
2269
|
-
catch (err) {
|
|
2270
|
-
console.error(chalk.red(err.message));
|
|
2271
|
-
process.exit(1);
|
|
2272
|
-
}
|
|
2273
|
-
// Slot / provisioned: binary comes from the one managed install unless `@<label>` pins it.
|
|
2274
|
-
if (!version && !fromProfile) {
|
|
2275
|
-
const { ensureHarnessInstallation } = await import('../lib/installations/store.js');
|
|
2276
|
-
const { installation } = await ensureHarnessInstallation(agent);
|
|
2277
|
-
version = installation.label;
|
|
2278
|
-
}
|
|
2279
|
-
if (!options.quiet)
|
|
2280
|
-
process.stderr.write(chalk.gray(`[agents] account '${spawnAccount.name}' · ${agent}\n`));
|
|
2281
2305
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2306
|
+
else {
|
|
2307
|
+
accountEnv = spawnAccount.env;
|
|
2308
|
+
}
|
|
2285
2309
|
}
|
|
2286
2310
|
}
|
|
2287
|
-
|
|
2288
|
-
//
|
|
2289
|
-
// FILTER (undefined for default/any, concrete for @latest/@oldest/@x.y.z);
|
|
2290
|
-
// it is replaced below by the chosen session's OWN version (isolation).
|
|
2311
|
+
const configuredAccount = launchAccount?.account?.name ?? spawnAccount?.name;
|
|
2312
|
+
// The account and native context were resolved before launch defaults.
|
|
2291
2313
|
let resumeNative = false;
|
|
2292
2314
|
let resumeSessionId;
|
|
2293
2315
|
let forceInteractive = false;
|
|
@@ -2300,136 +2322,23 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2300
2322
|
console.error(chalk.red('--resume cannot be combined with --loop, --fallback, or --resume-checkpoint (those are separate continuation mechanisms).'));
|
|
2301
2323
|
process.exit(1);
|
|
2302
2324
|
}
|
|
2303
|
-
const { findSessionsById } = await import('../lib/session/db.js');
|
|
2304
|
-
const { discoverSessions } = await import('../lib/session/discover.js');
|
|
2305
|
-
const { pickSessionInteractive } = await import('./sessions.js');
|
|
2306
2325
|
const { buildContinuePrompt } = await import('../lib/loop.js');
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
if (!
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
let scopeCwd;
|
|
2317
|
-
try {
|
|
2318
|
-
scopeCwd = fs.realpathSync(cwd);
|
|
2319
|
-
}
|
|
2320
|
-
catch {
|
|
2321
|
-
scopeCwd = cwd;
|
|
2322
|
-
}
|
|
2323
|
-
let session = resolvedResumeSource;
|
|
2324
|
-
if (idArg) {
|
|
2325
|
-
let matches = session ? [session] : findSessionsById(idArg, { agent: sessionAgent, version, cwd: scopeCwd });
|
|
2326
|
-
if (matches.length === 0) {
|
|
2327
|
-
const wide = findSessionsById(idArg, { agent: sessionAgent, version });
|
|
2328
|
-
if (wide.length > 0) {
|
|
2329
|
-
if (!options.quiet)
|
|
2330
|
-
process.stderr.write(chalk.gray(`No match for "${idArg}" in this project; widened to all projects.\n`));
|
|
2331
|
-
matches = wide;
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
if (matches.length === 0) {
|
|
2335
|
-
console.error(chalk.red(`No ${agent} session matching "${idArg}".`));
|
|
2336
|
-
console.error(chalk.gray(`Browse sessions: agents sessions ${idArg}`));
|
|
2337
|
-
process.exit(1);
|
|
2338
|
-
}
|
|
2339
|
-
else if (matches.length === 1) {
|
|
2340
|
-
session = matches[0];
|
|
2341
|
-
}
|
|
2342
|
-
else if (wantsInteractive) {
|
|
2343
|
-
const picked = await pickSessionInteractive(matches, `Multiple sessions match "${idArg}":`);
|
|
2344
|
-
if (!picked)
|
|
2345
|
-
process.exit(0);
|
|
2346
|
-
session = picked.session;
|
|
2347
|
-
}
|
|
2348
|
-
else {
|
|
2349
|
-
console.error(chalk.red(`"${idArg}" is ambiguous — ${matches.length} sessions match:`));
|
|
2350
|
-
for (const m of matches.slice(0, 10)) {
|
|
2351
|
-
console.error(chalk.gray(` ${m.shortId} ${m.timestamp.slice(0, 16).replace('T', ' ')} ${m.topic ?? m.label ?? ''}`));
|
|
2352
|
-
}
|
|
2353
|
-
console.error(chalk.gray('Pass more of the id, or resume interactively (drop the prompt).'));
|
|
2354
|
-
process.exit(1);
|
|
2355
|
-
}
|
|
2356
|
-
}
|
|
2357
|
-
else {
|
|
2358
|
-
// Bare --resume: pick from recent sessions in scope. Needs a TTY.
|
|
2359
|
-
if (!wantsInteractive) {
|
|
2360
|
-
console.error(chalk.red('--resume with no id needs an interactive terminal. Pass a session id (full or prefix), or run without --headless.'));
|
|
2361
|
-
process.exit(1);
|
|
2362
|
-
}
|
|
2363
|
-
const recent = await discoverSessions({ agent: sessionAgent, version, limit: 200 });
|
|
2364
|
-
if (recent.length === 0) {
|
|
2365
|
-
console.error(chalk.red(`No ${agent} sessions found to resume in this project.`));
|
|
2366
|
-
console.error(chalk.gray('Browse all: agents sessions'));
|
|
2367
|
-
process.exit(1);
|
|
2368
|
-
}
|
|
2369
|
-
const picked = await pickSessionInteractive(recent, `Resume which ${agent} session?`);
|
|
2370
|
-
if (!picked)
|
|
2371
|
-
process.exit(0);
|
|
2372
|
-
session = picked.session;
|
|
2373
|
-
forceInteractive = true; // bare resume always lands in the agent's TUI
|
|
2374
|
-
}
|
|
2375
|
-
// Bare `run <harness> --resume` learns the chosen SessionMeta only after
|
|
2376
|
-
// the host-placement phase above. If the picker chose a synced session
|
|
2377
|
-
// from another device, route the recovery command now; resolving local
|
|
2378
|
-
// candidates would otherwise native-resume through this device's
|
|
2379
|
-
// unrelated isolated home.
|
|
2380
|
-
if (!resolvedResumeSource) {
|
|
2381
|
-
const { sessionRecoveryPeer, sessionRecoveryRunArgs, } = await import('../lib/session/recovery.js');
|
|
2382
|
-
const peer = sessionRecoveryPeer(session);
|
|
2383
|
-
if (peer) {
|
|
2384
|
-
const { runOnPeer } = await import('../lib/session/remote-list.js');
|
|
2385
|
-
const routed = await runOnPeer(sessionRecoveryRunArgs(session), peer, { tty: true, sessionId: session.id });
|
|
2386
|
-
if (routed === 'no-target') {
|
|
2387
|
-
console.error(chalk.red(`Cannot recover session ${session.shortId}: origin device ${peer} is not a registered reachable peer.`));
|
|
2388
|
-
process.exitCode = 1;
|
|
2389
|
-
}
|
|
2390
|
-
return;
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
// Bare interactive --resume selects the SessionMeta only down here, so
|
|
2394
|
-
// it has not gone through the early concrete-id resolver. Resolve it now;
|
|
2395
|
-
// concrete ids reuse the exact same target chosen above.
|
|
2396
|
-
if (!resolvedRecoveryTarget) {
|
|
2397
|
-
try {
|
|
2398
|
-
const { resolveSessionRecovery } = await import('../lib/session/recovery.js');
|
|
2399
|
-
resolvedRecoveryTarget = await resolveSessionRecovery(session);
|
|
2400
|
-
}
|
|
2401
|
-
catch (err) {
|
|
2402
|
-
console.error(chalk.red(err.message));
|
|
2403
|
-
process.exit(1);
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2326
|
+
const session = resolvedResumeSource;
|
|
2327
|
+
if (!session || !resolvedRecoveryTarget) {
|
|
2328
|
+
throw new Error('Session recovery did not resolve a conversation on this device.');
|
|
2329
|
+
}
|
|
2330
|
+
if (!options.cwd)
|
|
2331
|
+
options.cwd = resolvedRecoveryTarget.mode === 'native'
|
|
2332
|
+
? resolvedRecoveryTarget.cwd ?? session.cwd
|
|
2333
|
+
: session.cwd;
|
|
2334
|
+
forceInteractive = resolveInteractive({ interactive: options.interactive, headless: options.headless, prompt });
|
|
2406
2335
|
if (resolvedRecoveryTarget.agent !== agent) {
|
|
2407
2336
|
console.error(chalk.red(`Session ${session.shortId} belongs to ${resolvedRecoveryTarget.agent}, not ${agent}. ` +
|
|
2408
2337
|
`Use: agents run auto --resume ${session.id}`));
|
|
2409
2338
|
process.exit(1);
|
|
2410
2339
|
}
|
|
2411
2340
|
version = resolvedRecoveryTarget.version;
|
|
2412
|
-
// Account rotation (PHNX-3626 / PHNX-3674): recovery picked a healthy
|
|
2413
|
-
// provider of the SAME harness. Inject that credential through the
|
|
2414
|
-
// `--account` path so spawn does not authenticate as the version home's
|
|
2415
|
-
// native login (the exhausted origin when the continue pick is a
|
|
2416
|
-
// provider). An explicit --account (configuredAccount) always wins.
|
|
2417
|
-
const rotatedAccount = resolvedRecoveryTarget.account;
|
|
2418
|
-
if (rotatedAccount && !configuredAccount) {
|
|
2419
|
-
try {
|
|
2420
|
-
const picked = resolveSpawnAccount(rotatedAccount.providerAccount, agent, version, readMeta(), { useDefault: false });
|
|
2421
|
-
if (picked?.kind === 'provider')
|
|
2422
|
-
accountEnv = picked.env;
|
|
2423
|
-
}
|
|
2424
|
-
catch {
|
|
2425
|
-
// Account-registry errors can carry provider credential material;
|
|
2426
|
-
// never relay them onto the terminal from this automatic path.
|
|
2427
|
-
console.error(chalk.red('Could not prepare the rotated provider account for session recovery.'));
|
|
2428
|
-
process.exit(1);
|
|
2429
|
-
}
|
|
2430
|
-
}
|
|
2431
2341
|
if (resolvedRecoveryTarget.mode === 'native') {
|
|
2432
|
-
version = session.version;
|
|
2433
2342
|
resumeNative = true;
|
|
2434
2343
|
resumeSessionId = session.id;
|
|
2435
2344
|
// The centralized recovery decision proves the transcript belongs to
|
|
@@ -2438,23 +2347,23 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2438
2347
|
// differ from the earlier cwd that selected projects/<cwd-key>.
|
|
2439
2348
|
if (!options.cwd && resolvedRecoveryTarget.cwd)
|
|
2440
2349
|
options.cwd = resolvedRecoveryTarget.cwd;
|
|
2441
|
-
if (rotatedAccount && !configuredAccount && !options.quiet) {
|
|
2442
|
-
process.stderr.write(chalk.gray(`[agents] origin ${agent} account limited → rotated to ${rotatedAccount.label} (native resume)\n`));
|
|
2443
|
-
}
|
|
2444
2350
|
if (!options.quiet)
|
|
2445
|
-
process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (native)
|
|
2351
|
+
process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (native) in ${options.cwd ?? cwd}\n`));
|
|
2446
2352
|
}
|
|
2447
2353
|
else {
|
|
2448
|
-
|
|
2449
|
-
|
|
2354
|
+
if (!isInteractiveTerminal() || options.headless) {
|
|
2355
|
+
console.error(chalk.red(`Native resume is unavailable: ${resolvedRecoveryTarget.reason}. To start a new conversation with this context, explicitly run: agents run ${agent} "/continue ${session.id}"`));
|
|
2356
|
+
process.exit(1);
|
|
2357
|
+
}
|
|
2358
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
2359
|
+
const replay = await confirm({ message: `${resolvedRecoveryTarget.reason}. Start a new conversation using this transcript?`, default: false }).catch(() => false);
|
|
2360
|
+
if (!replay)
|
|
2361
|
+
return;
|
|
2450
2362
|
prompt = buildContinuePrompt(session.id, prompt);
|
|
2451
2363
|
if (prompt.trim() === `/continue ${session.id}`)
|
|
2452
2364
|
forceInteractive = true;
|
|
2453
|
-
if (rotatedAccount && !configuredAccount && !options.quiet) {
|
|
2454
|
-
process.stderr.write(chalk.gray(`[agents] origin ${agent} account limited → rotated to ${rotatedAccount.label} (/continue)\n`));
|
|
2455
|
-
}
|
|
2456
2365
|
if (!options.quiet)
|
|
2457
|
-
process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (/continue replay)
|
|
2366
|
+
process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (/continue replay)\n`));
|
|
2458
2367
|
}
|
|
2459
2368
|
}
|
|
2460
2369
|
const configuredStrategy = getConfiguredRunStrategy(agent, cwd);
|
|
@@ -2511,7 +2420,9 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2511
2420
|
// picker's other callers keep the native-only collector.
|
|
2512
2421
|
const { collectRunCandidatesForRun } = await import('../lib/accounting/account-pool-collect.js');
|
|
2513
2422
|
bootMark('resolve-version:start');
|
|
2514
|
-
const
|
|
2423
|
+
const routingModel = options.model ?? resolvedResumeSource?.model ?? workflowModel
|
|
2424
|
+
?? (options.fallback ? undefined : resolveRunDefaults(agent, resolveVersion(agent, cwd), cwd).model);
|
|
2425
|
+
const resolved = await resolveRunVersion(agent, strategy, cwd, collectRunCandidatesForRun, routingModel);
|
|
2515
2426
|
bootMark('resolve-version:done');
|
|
2516
2427
|
if (resolved.exhausted) {
|
|
2517
2428
|
// Zero healthy accounts splits two ways, and conflating them is what
|
|
@@ -2665,17 +2576,6 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2665
2576
|
console.error(chalk.red(`agents: ${agent}@${launchTarget} is not runnable and could not be repaired. Try: ${hint}`));
|
|
2666
2577
|
process.exit(1);
|
|
2667
2578
|
}
|
|
2668
|
-
if (resolvedRecoveryTarget && healed !== launchTarget) {
|
|
2669
|
-
console.error(chalk.red(`agents: session recovery target ${agent}@${launchTarget} became unavailable on ` +
|
|
2670
|
-
`${resolvedResumeSource?.machine ?? 'this device'}; refusing to resume through another version home. ` +
|
|
2671
|
-
`Retry the command so recovery can select a healthy ${agent} version.`));
|
|
2672
|
-
process.exit(1);
|
|
2673
|
-
}
|
|
2674
|
-
// Always adopt the healed version explicitly. In the version-undefined
|
|
2675
|
-
// path a fallback re-pins the GLOBAL default, but `resolveVersion`
|
|
2676
|
-
// prefers a PROJECT pin — so leaving `version` undefined would let the
|
|
2677
|
-
// shim re-resolve the still-broken project pin and crash anyway. Pinning
|
|
2678
|
-
// the runnable version here is a no-op when nothing changed.
|
|
2679
2579
|
version = healed;
|
|
2680
2580
|
}
|
|
2681
2581
|
}
|
|
@@ -2741,13 +2641,13 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2741
2641
|
// `signInLaunch` means the zero-healthy path already reported this exact
|
|
2742
2642
|
// account as logged out and named the login command, so re-probing here
|
|
2743
2643
|
// only prints a second, near-identical warning.
|
|
2744
|
-
rotated: !!rotationResult || accountPickerRequested || signInLaunch || spawnAccount?.kind === 'provider',
|
|
2644
|
+
rotated: !!rotationResult || accountPickerRequested || signInLaunch || (launchAccount?.account?.kind ?? spawnAccount?.kind) === 'provider',
|
|
2745
2645
|
});
|
|
2746
2646
|
if (preflight) {
|
|
2747
2647
|
try {
|
|
2748
2648
|
const { getAccountInfo } = await import('../lib/agents.js');
|
|
2749
2649
|
const authVersion = accountConfigVersion ?? version;
|
|
2750
|
-
const info = await getAccountInfo(agent, authVersion ? getVersionHomePath(agent, authVersion) : undefined);
|
|
2650
|
+
const info = await getAccountInfo(agent, execHome ?? (authVersion ? getVersionHomePath(agent, authVersion) : undefined));
|
|
2751
2651
|
// Claude authenticates interactively from a per-version setup-token on a
|
|
2752
2652
|
// keychain-less worker (the shim's .oauth_token fallback), which the
|
|
2753
2653
|
// native-credential probe above can't see — so don't warn "logged out" when
|
|
@@ -2761,8 +2661,8 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2761
2661
|
}
|
|
2762
2662
|
if (!info.signedIn && !authedViaSetupToken) {
|
|
2763
2663
|
const { addSupported } = await import('../lib/accounts/add.js');
|
|
2764
|
-
const hint = spawnAccount?.kind === 'native'
|
|
2765
|
-
? `agents accounts login ${agent}#${
|
|
2664
|
+
const hint = (launchAccount?.account?.kind ?? spawnAccount?.kind) === 'native'
|
|
2665
|
+
? `agents accounts login ${agent}#${configuredAccount}`
|
|
2766
2666
|
: addSupported(agent)
|
|
2767
2667
|
? `agents accounts add ${agent} <name>`
|
|
2768
2668
|
: loginHint(agent);
|
|
@@ -2823,7 +2723,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2823
2723
|
// `--mode plan` is respected for genuine read-only command runs.
|
|
2824
2724
|
const stallCmd = headlessPlanStallCommand({
|
|
2825
2725
|
prompt,
|
|
2826
|
-
interactive: options.interactive,
|
|
2726
|
+
interactive: resolveInteractive({ prompt, headless: options.headless, interactive: options.interactive || forceInteractive }),
|
|
2827
2727
|
mode: resolvedMode,
|
|
2828
2728
|
modeIsDefault,
|
|
2829
2729
|
});
|
|
@@ -2918,6 +2818,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2918
2818
|
: undefined;
|
|
2919
2819
|
const modelSource = runCmd.getOptionValueSource('model');
|
|
2920
2820
|
let model = options.model
|
|
2821
|
+
?? resolvedResumeSource?.model
|
|
2921
2822
|
?? (!fromProfile && modelSource === undefined
|
|
2922
2823
|
? (workflowModel ?? (options.fallback ? undefined : runDefaults.model))
|
|
2923
2824
|
: undefined);
|
|
@@ -2933,6 +2834,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2933
2834
|
const execOptions = {
|
|
2934
2835
|
agent,
|
|
2935
2836
|
harnessName: profileName,
|
|
2837
|
+
accountId: launchAccount?.account?.kind === 'legacy-native' ? undefined : (launchAccount?.account?.id ?? spawnAccount?.id),
|
|
2936
2838
|
version,
|
|
2937
2839
|
configVersion: accountConfigVersion,
|
|
2938
2840
|
execHome,
|