@phnx-labs/agents-cli 1.20.49 → 1.20.50
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 +7 -0
- package/README.md +3 -0
- package/dist/commands/doctor.js +133 -2
- package/dist/commands/teams.js +217 -70
- package/dist/lib/exec.js +12 -4
- package/dist/lib/hosts/passthrough.js +30 -1
- package/dist/lib/hosts/progress.d.ts +31 -0
- package/dist/lib/hosts/progress.js +35 -0
- package/dist/lib/hosts/remote-cmd.d.ts +1 -1
- package/dist/lib/hosts/remote-cmd.js +13 -3
- package/dist/lib/shims.d.ts +17 -0
- package/dist/lib/shims.js +29 -0
- package/dist/lib/teams/agents.d.ts +108 -1
- package/dist/lib/teams/agents.js +511 -11
- package/dist/lib/teams/api.d.ts +7 -1
- package/dist/lib/teams/api.js +5 -2
- package/dist/lib/teams/registry.d.ts +17 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/remoteWorktree.d.ts +57 -0
- package/dist/lib/teams/remoteWorktree.js +213 -0
- package/dist/lib/teams/scheduler.d.ts +29 -0
- package/dist/lib/teams/scheduler.js +78 -0
- package/dist/lib/teams/supervisor.js +7 -0
- package/dist/lib/versions.d.ts +7 -0
- package/dist/lib/versions.js +42 -1
- package/package.json +1 -1
package/dist/lib/exec.js
CHANGED
|
@@ -16,6 +16,7 @@ import { resolveModel, buildReasoningFlags } from './models.js';
|
|
|
16
16
|
import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
|
|
17
17
|
import { sanitizeProcessEnv } from './secrets/bundles.js';
|
|
18
18
|
import { getShimsDir } from './state.js';
|
|
19
|
+
import { readCodexConfiguredModel } from './shims.js';
|
|
19
20
|
import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
|
|
20
21
|
import { recordRunName } from './session/run-names.js';
|
|
21
22
|
import { mailboxDir, isValidMailboxId } from './mailbox.js';
|
|
@@ -610,18 +611,25 @@ export function buildExecCommand(options) {
|
|
|
610
611
|
else if (options.sessionId && options.agent === 'claude') {
|
|
611
612
|
cmd.push('--session-id', options.sessionId);
|
|
612
613
|
}
|
|
613
|
-
// Add model
|
|
614
|
-
|
|
614
|
+
// Add model. Prefer the user's explicit --model. Otherwise, for Codex, fall
|
|
615
|
+
// back to the model configured in the user's active ~/.codex/config.toml:
|
|
616
|
+
// Codex runs under a per-version CODEX_HOME (see buildExecEnv) that may not
|
|
617
|
+
// carry that setting, so without this it silently defaults to gpt-5.3-codex,
|
|
618
|
+
// which a ChatGPT-tier account can't use (HTTP 400). Forwarding keeps the
|
|
619
|
+
// user's default model setup for both `agents run` and `agents teams`.
|
|
620
|
+
const effectiveModel = options.model
|
|
621
|
+
?? (options.agent === 'codex' ? readCodexConfiguredModel() : undefined);
|
|
622
|
+
if (effectiveModel && template.modelFlag) {
|
|
615
623
|
const effectiveVersion = options.version || resolveVersion(options.agent, options.cwd || process.cwd());
|
|
616
624
|
if (effectiveVersion) {
|
|
617
|
-
const resolved = resolveModel(options.agent, effectiveVersion,
|
|
625
|
+
const resolved = resolveModel(options.agent, effectiveVersion, effectiveModel);
|
|
618
626
|
if (resolved.warning) {
|
|
619
627
|
process.stderr.write(`[agents] ${resolved.warning}\n`);
|
|
620
628
|
}
|
|
621
629
|
cmd.push(template.modelFlag, resolved.forwarded);
|
|
622
630
|
}
|
|
623
631
|
else {
|
|
624
|
-
cmd.push(template.modelFlag,
|
|
632
|
+
cmd.push(template.modelFlag, effectiveModel);
|
|
625
633
|
}
|
|
626
634
|
}
|
|
627
635
|
// Add JSON output flags if requested
|
|
@@ -91,6 +91,20 @@ export async function maybeRunOnHost(command, allArgs) {
|
|
|
91
91
|
const spec = REMOTE_PASSTHROUGH[command];
|
|
92
92
|
if (!spec)
|
|
93
93
|
return false;
|
|
94
|
+
// Placement, not routing: `teams add`/`teams create` read `--device`/`--devices`
|
|
95
|
+
// (and `--host`/`--hosts`) as WHERE to place a teammate / the team pool — the
|
|
96
|
+
// command itself always runs locally on the orchestrator. Bail before the
|
|
97
|
+
// generic teams routing below so those flags reach the local action. Every
|
|
98
|
+
// other teams subcommand (`status`/`logs`/`stop`/…) keeps `--host` routing.
|
|
99
|
+
// Find the subcommand = the first non-flag token AFTER `teams` (robust to any
|
|
100
|
+
// leading global flags), then bail for the add/create aliases.
|
|
101
|
+
if (command === 'teams') {
|
|
102
|
+
const teamsIdx = allArgs.indexOf('teams');
|
|
103
|
+
const sub = teamsIdx >= 0 ? allArgs.slice(teamsIdx + 1).find((a) => !a.startsWith('-')) : undefined;
|
|
104
|
+
if (sub === 'add' || sub === 'a' || sub === 'create' || sub === 'c' || sub === 'new') {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
94
108
|
// `--device` is a first-class alias of `--host` (mirrors `agents run`); the
|
|
95
109
|
// device registry is the source of truth for machine identity. Reject a
|
|
96
110
|
// conflicting pair rather than silently preferring one — same rule as run.
|
|
@@ -101,6 +115,11 @@ export async function maybeRunOnHost(command, allArgs) {
|
|
|
101
115
|
process.exitCode = 1;
|
|
102
116
|
return true;
|
|
103
117
|
}
|
|
118
|
+
// `--devices` / `--hosts` fan out to every registered device locally; don't
|
|
119
|
+
// let a per-host passthrough turn it into a cascading remote fan-out.
|
|
120
|
+
const fleetFlag = allArgs.includes('--devices') || allArgs.includes('--hosts');
|
|
121
|
+
if (fleetFlag)
|
|
122
|
+
return false;
|
|
104
123
|
const hostName = hostFlag ?? deviceFlag;
|
|
105
124
|
if (!hostName)
|
|
106
125
|
return false;
|
|
@@ -142,7 +161,17 @@ export async function maybeRunOnHost(command, allArgs) {
|
|
|
142
161
|
}
|
|
143
162
|
return true;
|
|
144
163
|
}
|
|
145
|
-
|
|
164
|
+
// Doctor commands probe the agent CLIs; remote POSIX login shells often don't
|
|
165
|
+
// have the agents shims on PATH, which produces false "not installed" negatives.
|
|
166
|
+
// Bootstrap PATH with the canonical shim locations before the remote command.
|
|
167
|
+
// Windows is skipped: PowerShell usually has the shim dir via the install
|
|
168
|
+
// profile, and single-quoted env values would not expand $HOME/$PATH.
|
|
169
|
+
const isDoctorCommand = command === 'doctor' || (command === 'teams' && forwarded[1] === 'doctor');
|
|
170
|
+
const remoteOs = resolveRemoteOsSync(host.name);
|
|
171
|
+
const env = isDoctorCommand && !/^win/i.test((remoteOs ?? '').trim())
|
|
172
|
+
? { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' }
|
|
173
|
+
: undefined;
|
|
174
|
+
const remoteCmd = buildRemoteAgentsInvocation(forwarded, remoteCwd, remoteOs, env);
|
|
146
175
|
const code = sshStream(target, remoteCmd, { tty: interactive, multiplex: true });
|
|
147
176
|
if (code === 255) {
|
|
148
177
|
console.error(chalk.red(`${host.name}: unreachable over SSH (asleep, offline, or host key changed?).`) +
|
|
@@ -13,6 +13,37 @@
|
|
|
13
13
|
* toward `maxPollMs` while the job is idle, so a quiet long-running follow no
|
|
14
14
|
* longer spawns thousands of ssh processes per hour on the laptop.
|
|
15
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* Cap for the LOCAL mirror of a distributed teammate's remote log. `followHostTask`
|
|
18
|
+
* appends remote bytes into the local mirror forever; a team can spin 10+ chatty
|
|
19
|
+
* remote teammates, so the orchestrator must keep a bounded window (the full log
|
|
20
|
+
* always lives on the host). The teams remote path (agents.ts readNewEvents) writes
|
|
21
|
+
* its own append into each teammate's `stdout.log`, then truncates that file to its
|
|
22
|
+
* trailing `REMOTE_MIRROR_MAX_BYTES` — the parser has already consumed the bytes
|
|
23
|
+
* (status/digest updated via lastReadPos), so trailing-tail history is dead weight.
|
|
24
|
+
*/
|
|
25
|
+
export declare const REMOTE_MIRROR_MAX_BYTES: number;
|
|
26
|
+
/**
|
|
27
|
+
* Pull the new bytes of a remote log since `offset` in ONE ssh round-trip.
|
|
28
|
+
*
|
|
29
|
+
* The teams remote-teammate monitor calls this each poll to advance its offset-tail
|
|
30
|
+
* cursor into the host's log, mirroring only the delta into the local `stdout.log`
|
|
31
|
+
* the stream-json parser consumes. Byte-exact (raw Buffer, no UTF-8 decode) so a
|
|
32
|
+
* multibyte character split at the `tail -c` boundary neither drifts the offset nor
|
|
33
|
+
* renders as U+FFFD — the same discipline `fetchProgress` uses. `bytes.length` is
|
|
34
|
+
* the exact wire count; `newOffset` is `offset + bytes.length`.
|
|
35
|
+
*
|
|
36
|
+
* Returns null on a transient ssh failure (the caller retries next poll without
|
|
37
|
+
* advancing). `remoteLog` is a $HOME-prefixed path with a safe basename; it's
|
|
38
|
+
* shell-quoted defensively even so.
|
|
39
|
+
*/
|
|
40
|
+
export declare function pullRemoteLogDelta(target: string, opts: {
|
|
41
|
+
remoteLog: string;
|
|
42
|
+
offset: number;
|
|
43
|
+
}): {
|
|
44
|
+
bytes: Buffer;
|
|
45
|
+
newOffset: number;
|
|
46
|
+
} | null;
|
|
16
47
|
export interface FollowOptions {
|
|
17
48
|
remoteLog: string;
|
|
18
49
|
remoteExit: string;
|
|
@@ -19,6 +19,41 @@ import { localLogPath } from './tasks.js';
|
|
|
19
19
|
function sleep(ms) {
|
|
20
20
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Cap for the LOCAL mirror of a distributed teammate's remote log. `followHostTask`
|
|
24
|
+
* appends remote bytes into the local mirror forever; a team can spin 10+ chatty
|
|
25
|
+
* remote teammates, so the orchestrator must keep a bounded window (the full log
|
|
26
|
+
* always lives on the host). The teams remote path (agents.ts readNewEvents) writes
|
|
27
|
+
* its own append into each teammate's `stdout.log`, then truncates that file to its
|
|
28
|
+
* trailing `REMOTE_MIRROR_MAX_BYTES` — the parser has already consumed the bytes
|
|
29
|
+
* (status/digest updated via lastReadPos), so trailing-tail history is dead weight.
|
|
30
|
+
*/
|
|
31
|
+
export const REMOTE_MIRROR_MAX_BYTES = 512 * 1024;
|
|
32
|
+
/**
|
|
33
|
+
* Pull the new bytes of a remote log since `offset` in ONE ssh round-trip.
|
|
34
|
+
*
|
|
35
|
+
* The teams remote-teammate monitor calls this each poll to advance its offset-tail
|
|
36
|
+
* cursor into the host's log, mirroring only the delta into the local `stdout.log`
|
|
37
|
+
* the stream-json parser consumes. Byte-exact (raw Buffer, no UTF-8 decode) so a
|
|
38
|
+
* multibyte character split at the `tail -c` boundary neither drifts the offset nor
|
|
39
|
+
* renders as U+FFFD — the same discipline `fetchProgress` uses. `bytes.length` is
|
|
40
|
+
* the exact wire count; `newOffset` is `offset + bytes.length`.
|
|
41
|
+
*
|
|
42
|
+
* Returns null on a transient ssh failure (the caller retries next poll without
|
|
43
|
+
* advancing). `remoteLog` is a $HOME-prefixed path with a safe basename; it's
|
|
44
|
+
* shell-quoted defensively even so.
|
|
45
|
+
*/
|
|
46
|
+
export function pullRemoteLogDelta(target, opts) {
|
|
47
|
+
// remoteLog is a dispatch-generated `$HOME/.agents/.cache/hosts/<hex>.log` path —
|
|
48
|
+
// interpolate UNQUOTED so the remote shell expands `$HOME` (shellQuote would
|
|
49
|
+
// single-quote it into a literal `$HOME`, and the tail would find nothing). The
|
|
50
|
+
// hex basename is injection-safe, matching fetchProgress below.
|
|
51
|
+
const remote = `tail -c +${opts.offset + 1} ${opts.remoteLog} 2>/dev/null`;
|
|
52
|
+
const res = sshExecRaw(target, remote, { timeoutMs: 20000, multiplex: true });
|
|
53
|
+
if (res.code === null)
|
|
54
|
+
return null; // ssh itself failed / timed out
|
|
55
|
+
return { bytes: res.stdout, newOffset: opts.offset + res.stdout.length };
|
|
56
|
+
}
|
|
22
57
|
/**
|
|
23
58
|
* Build the per-task sentinel that separates the log tail from the exit-file
|
|
24
59
|
* contents in one combined fetch. The task id (8 hex chars) makes collision with
|
|
@@ -44,7 +44,7 @@ export declare const HOST_ROUTING_SPECS: StripSpec[];
|
|
|
44
44
|
* does not exist). Anything else — including an unknown/absent OS — keeps the
|
|
45
45
|
* POSIX form, so linux/macos are byte-for-byte unchanged.
|
|
46
46
|
*/
|
|
47
|
-
export declare function buildRemoteAgentsInvocation(forwardedArgs: string[], remoteCwd?: string, os?: string): string;
|
|
47
|
+
export declare function buildRemoteAgentsInvocation(forwardedArgs: string[], remoteCwd?: string, os?: string, env?: Record<string, string>): string;
|
|
48
48
|
/** The two remote shell dialects we build commands for. */
|
|
49
49
|
export type RemoteShell = 'posix' | 'powershell';
|
|
50
50
|
/**
|
|
@@ -61,13 +61,23 @@ export const HOST_ROUTING_SPECS = [
|
|
|
61
61
|
* does not exist). Anything else — including an unknown/absent OS — keeps the
|
|
62
62
|
* POSIX form, so linux/macos are byte-for-byte unchanged.
|
|
63
63
|
*/
|
|
64
|
-
export function buildRemoteAgentsInvocation(forwardedArgs, remoteCwd, os) {
|
|
64
|
+
export function buildRemoteAgentsInvocation(forwardedArgs, remoteCwd, os, env) {
|
|
65
65
|
if (remoteShellFor(os) === 'powershell') {
|
|
66
|
-
return buildWindowsAgentsCommand({ args: forwardedArgs, cwd: remoteCwd });
|
|
66
|
+
return buildWindowsAgentsCommand({ args: forwardedArgs, cwd: remoteCwd, env });
|
|
67
67
|
}
|
|
68
68
|
const inner = ['agents', ...forwardedArgs].map(shellQuote).join(' ');
|
|
69
69
|
const withCwd = remoteCwd ? `cd ${shellQuote(remoteCwd)} && ${inner}` : inner;
|
|
70
|
-
|
|
70
|
+
if (!env || Object.keys(env).length === 0) {
|
|
71
|
+
return `bash -lc ${shellQuote(withCwd)}`;
|
|
72
|
+
}
|
|
73
|
+
// Prepend env exports so the remote command sees the shims dir even when the
|
|
74
|
+
// login shell hasn't sourced the interactive rc files that usually add it.
|
|
75
|
+
// Values are double-quoted (not single-quoted) so remote variables like
|
|
76
|
+
// $HOME and $PATH are expanded by the login shell.
|
|
77
|
+
const exports = Object.entries(env)
|
|
78
|
+
.map(([k, v]) => `export ${shellQuote(k)}="${v.replace(/[\\"]/g, '\\$&')}"`)
|
|
79
|
+
.join('; ');
|
|
80
|
+
return `bash -lc ${shellQuote(`${exports}; ${withCwd}`)}`;
|
|
71
81
|
}
|
|
72
82
|
/**
|
|
73
83
|
* Pick the remote shell dialect from a recorded OS/platform string. A Windows
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -203,6 +203,23 @@ export declare function removeVersionedAlias(agent: AgentId, version: string): b
|
|
|
203
203
|
* Check if a versioned alias exists (the on-disk artifact for this platform).
|
|
204
204
|
*/
|
|
205
205
|
export declare function versionedAliasExists(agent: AgentId, version: string): boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Read the user's configured Codex model from their active `~/.codex/config.toml`.
|
|
208
|
+
*
|
|
209
|
+
* Codex runs under a per-version `CODEX_HOME` (see `buildExecEnv`). A dispatch
|
|
210
|
+
* pinned to a version whose home config lacks a top-level `model` key silently
|
|
211
|
+
* falls back to Codex's built-in default (currently `gpt-5.3-codex`), which a
|
|
212
|
+
* ChatGPT-tier account is not entitled to use — the run dies with HTTP 400
|
|
213
|
+
* before doing any work. The user's model preference lives in whichever
|
|
214
|
+
* version-home was active when they set it (`~/.codex` symlinks to it), so it is
|
|
215
|
+
* NOT visible to a run pinned to a different version. Forwarding it via `--model`
|
|
216
|
+
* keeps the user's "default model setup" regardless of the active version-home,
|
|
217
|
+
* and is concurrency-safe (no file writes) when fanning out many parallel runs.
|
|
218
|
+
*
|
|
219
|
+
* Returns the top-level `model` only (ignores `[profile.*]` tables). Best-effort:
|
|
220
|
+
* a missing/unreadable/unset config yields `undefined` (caller keeps prior behaviour).
|
|
221
|
+
*/
|
|
222
|
+
export declare function readCodexConfiguredModel(): string | undefined;
|
|
206
223
|
/**
|
|
207
224
|
* Switch the agent's config symlink to point to a specific version.
|
|
208
225
|
* e.g., ~/.claude -> ~/.agents/versions/claude/2.0.65/home/.claude/
|
package/dist/lib/shims.js
CHANGED
|
@@ -1008,6 +1008,35 @@ function getAgentConfigPath(agent) {
|
|
|
1008
1008
|
const home = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
1009
1009
|
return agentConfig.configDir.replace(os.homedir(), home);
|
|
1010
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Read the user's configured Codex model from their active `~/.codex/config.toml`.
|
|
1013
|
+
*
|
|
1014
|
+
* Codex runs under a per-version `CODEX_HOME` (see `buildExecEnv`). A dispatch
|
|
1015
|
+
* pinned to a version whose home config lacks a top-level `model` key silently
|
|
1016
|
+
* falls back to Codex's built-in default (currently `gpt-5.3-codex`), which a
|
|
1017
|
+
* ChatGPT-tier account is not entitled to use — the run dies with HTTP 400
|
|
1018
|
+
* before doing any work. The user's model preference lives in whichever
|
|
1019
|
+
* version-home was active when they set it (`~/.codex` symlinks to it), so it is
|
|
1020
|
+
* NOT visible to a run pinned to a different version. Forwarding it via `--model`
|
|
1021
|
+
* keeps the user's "default model setup" regardless of the active version-home,
|
|
1022
|
+
* and is concurrency-safe (no file writes) when fanning out many parallel runs.
|
|
1023
|
+
*
|
|
1024
|
+
* Returns the top-level `model` only (ignores `[profile.*]` tables). Best-effort:
|
|
1025
|
+
* a missing/unreadable/unset config yields `undefined` (caller keeps prior behaviour).
|
|
1026
|
+
*/
|
|
1027
|
+
export function readCodexConfiguredModel() {
|
|
1028
|
+
try {
|
|
1029
|
+
const cfg = path.join(getAgentConfigPath('codex'), 'config.toml');
|
|
1030
|
+
const text = fs.readFileSync(cfg, 'utf-8');
|
|
1031
|
+
// Only trust keys before the first [table]; a `model` under [profile.x] is
|
|
1032
|
+
// not the default the CLI uses at top level.
|
|
1033
|
+
const topLevel = text.split(/^\s*\[/m)[0];
|
|
1034
|
+
return topLevel.match(/^\s*model\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
1035
|
+
}
|
|
1036
|
+
catch {
|
|
1037
|
+
return undefined;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1011
1040
|
/**
|
|
1012
1041
|
* Get the path to the version's config directory.
|
|
1013
1042
|
* e.g., ~/.agents/versions/claude/2.0.65/home/.claude/
|
|
@@ -92,6 +92,21 @@ export interface SignInAdvisory {
|
|
|
92
92
|
* probe). Never flips the authoritative installed/ready column.
|
|
93
93
|
*/
|
|
94
94
|
export declare function resolveSignInAdvisory(installed: boolean, running: boolean, probeSignedIn: boolean): SignInAdvisory;
|
|
95
|
+
/** One row of `agents teams doctor --json` output. */
|
|
96
|
+
export interface TeamsDoctorEntry {
|
|
97
|
+
installed: boolean;
|
|
98
|
+
path: string | null;
|
|
99
|
+
error: string | null;
|
|
100
|
+
signedIn: boolean | null;
|
|
101
|
+
running: boolean;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Collect the same data `agents teams doctor` prints: per-agent install status,
|
|
105
|
+
* launch health, and advisory sign-in state. Kept in one place so `agents doctor
|
|
106
|
+
* --devices` can run it locally or compare it against remote JSON without
|
|
107
|
+
* duplicating the probe logic.
|
|
108
|
+
*/
|
|
109
|
+
export declare function collectTeamsDoctorData(): Promise<Record<string, TeamsDoctorEntry>>;
|
|
95
110
|
/** Resolve and cache the base directory where teammate process data is stored. */
|
|
96
111
|
export declare function getAgentsDir(): Promise<string>;
|
|
97
112
|
/**
|
|
@@ -131,6 +146,17 @@ export declare class AgentProcess {
|
|
|
131
146
|
cloudBranch: string | null;
|
|
132
147
|
worktreeName: string | null;
|
|
133
148
|
worktreePath: string | null;
|
|
149
|
+
hostName: string | null;
|
|
150
|
+
hostTarget: string | null;
|
|
151
|
+
repoPath: string | null;
|
|
152
|
+
remotePid: number | null;
|
|
153
|
+
remoteLog: string | null;
|
|
154
|
+
remoteExit: string | null;
|
|
155
|
+
remoteLogOffset: number;
|
|
156
|
+
remotePollSnapshot: {
|
|
157
|
+
alive: boolean;
|
|
158
|
+
exit: string | null;
|
|
159
|
+
} | null;
|
|
134
160
|
private eventsCache;
|
|
135
161
|
private lastReadPos;
|
|
136
162
|
private baseDir;
|
|
@@ -175,7 +201,35 @@ export declare class AgentProcess {
|
|
|
175
201
|
* Falls back to null when none are available.
|
|
176
202
|
*/
|
|
177
203
|
private getLatestEventTime;
|
|
204
|
+
/**
|
|
205
|
+
* For a distributed (remote-host) teammate, pull NEW bytes of the host's log
|
|
206
|
+
* into the LOCAL mirror the parser consumes, advance the remote offset, and
|
|
207
|
+
* resolve terminal status from the remote `.exit` sentinel. Runs BEFORE the
|
|
208
|
+
* local read in readNewEvents(), so the existing stream-json parse path then
|
|
209
|
+
* runs unchanged over the freshly-mirrored bytes.
|
|
210
|
+
*
|
|
211
|
+
* Uses a per-wave batched snapshot (remotePollSnapshot) when the supervisor's
|
|
212
|
+
* one-ssh-per-host pre-pass populated it; otherwise falls back to its own
|
|
213
|
+
* round-trips so a bare `teams status`/`teams logs` is still correct.
|
|
214
|
+
*/
|
|
215
|
+
private syncRemoteMirror;
|
|
178
216
|
readNewEvents(): Promise<void>;
|
|
217
|
+
/**
|
|
218
|
+
* Truncate the local mirror to its trailing REMOTE_MIRROR_MAX_BYTES and reset
|
|
219
|
+
* lastReadPos to the new (smaller) size so the parser doesn't re-read the kept
|
|
220
|
+
* tail. Only trims when over the cap — a normal-length log is untouched.
|
|
221
|
+
*/
|
|
222
|
+
private capMirrorToTail;
|
|
223
|
+
/** Cap on the in-memory event backlog kept per remote teammate. */
|
|
224
|
+
private static readonly REMOTE_EVENTS_MAX;
|
|
225
|
+
/**
|
|
226
|
+
* Drop the oldest cached events for a remote teammate once past the cap. The
|
|
227
|
+
* status path only needs recent events (last N messages, recentToolCalls,
|
|
228
|
+
* terminal status) and the getDelta cursor filters by timestamp, so a bounded
|
|
229
|
+
* recent window preserves the digest while bounding the heap. Terminal status
|
|
230
|
+
* is already latched onto `this.status`, so trimming can't lose it.
|
|
231
|
+
*/
|
|
232
|
+
private capEventsCache;
|
|
179
233
|
saveMeta(): Promise<void>;
|
|
180
234
|
static loadFromDisk(agentId: string, baseDir?: string | null): Promise<AgentProcess | null>;
|
|
181
235
|
isProcessAlive(): boolean;
|
|
@@ -246,12 +300,51 @@ export declare class AgentManager {
|
|
|
246
300
|
*/
|
|
247
301
|
rescanFromDisk(): Promise<number>;
|
|
248
302
|
private loadExistingAgents;
|
|
249
|
-
spawn(taskName: string, agentType: AgentType, prompt: string, cwd?: string | null, mode?: Mode | null, effort?: EffortLevel, parentSessionId?: string | null, workspaceDir?: string | null, version?: string | null, name?: string | null, after?: string[], model?: string | null, envOverrides?: Record<string, string> | null, taskType?: TaskType | null, cloudProvider?: string | null, cloudSessionId?: string | null, cloudRepo?: string | null, cloudBranch?: string | null, worktreeName?: string | null, worktreePath?: string | null, profileName?: string | null): Promise<AgentProcess>;
|
|
303
|
+
spawn(taskName: string, agentType: AgentType, prompt: string, cwd?: string | null, mode?: Mode | null, effort?: EffortLevel, parentSessionId?: string | null, workspaceDir?: string | null, version?: string | null, name?: string | null, after?: string[], model?: string | null, envOverrides?: Record<string, string> | null, taskType?: TaskType | null, cloudProvider?: string | null, cloudSessionId?: string | null, cloudRepo?: string | null, cloudBranch?: string | null, worktreeName?: string | null, worktreePath?: string | null, profileName?: string | null, hostName?: string | null, hostTarget?: string | null, repoPath?: string | null): Promise<AgentProcess>;
|
|
250
304
|
/**
|
|
251
305
|
* Actually spawn the OS process for a teammate. Extracted from spawn() so
|
|
252
306
|
* staged teammates can be launched later by startReady().
|
|
253
307
|
*/
|
|
254
308
|
private launchProcess;
|
|
309
|
+
/**
|
|
310
|
+
* Dispatch a distributed teammate onto its host over SSH — the remote-host
|
|
311
|
+
* analog of launchProcess(). Symmetric to the cloud path: no local process; the
|
|
312
|
+
* lifecycle lives on the host and is polled (isProcessAlive/readNewEvents over
|
|
313
|
+
* SSH via the remote `.exit` sentinel + offset-tailed log).
|
|
314
|
+
*
|
|
315
|
+
* When the team uses worktrees (agent.worktreeName set), a git worktree is first
|
|
316
|
+
* created ON THE HOST off the freshly-fetched default branch; the teammate runs
|
|
317
|
+
* there. Otherwise it runs in the host repo path directly.
|
|
318
|
+
*/
|
|
319
|
+
private launchRemoteProcess;
|
|
320
|
+
/**
|
|
321
|
+
* Resolve a scheduler-picked device to host placement fields on an unpinned
|
|
322
|
+
* teammate at LAUNCH time (the same resolution `teams add --device` runs, minus
|
|
323
|
+
* the fatal `die()` — a scheduling failure here is per-teammate, not per-add).
|
|
324
|
+
* Sets hostName/hostTarget/repoPath + persists, so the subsequent
|
|
325
|
+
* launchRemoteProcess dispatches over SSH. Mirrors the `add`-time pin path:
|
|
326
|
+
* resolve device → reject Windows (POSIX-only) → ssh target → ensure the repo
|
|
327
|
+
* is present on the host from the team's --repo (ensureRemoteRepo).
|
|
328
|
+
*/
|
|
329
|
+
private resolveScheduledPlacement;
|
|
330
|
+
/**
|
|
331
|
+
* Place an UNPINNED, non-cloud teammate onto the team pool via the cascade
|
|
332
|
+
* (least-loaded), if the team declares one. A no-op for a pinned teammate
|
|
333
|
+
* (hostName already set from `--device`), a cloud teammate, or a poolless team —
|
|
334
|
+
* leaving hostName null so the local spawn runs unchanged. Shared by spawn()
|
|
335
|
+
* (immediate add-launch) and startReady() (staged launch) so an unpinned pool
|
|
336
|
+
* teammate schedules identically no matter how it was fired.
|
|
337
|
+
*/
|
|
338
|
+
private maybeSchedulePlacement;
|
|
339
|
+
/**
|
|
340
|
+
* One-ssh-per-host batched liveness/exit pre-pass for a team's remote teammates.
|
|
341
|
+
* The supervisor calls this each wave BEFORE listByTask() so the per-teammate
|
|
342
|
+
* isProcessAlive()/readNewEvents() consume a cached snapshot instead of each
|
|
343
|
+
* issuing its own SSH handshake — avoiding N round-trips per wave at 10+ remote
|
|
344
|
+
* teammates. Groups by hostTarget and, for each host, checks every teammate's
|
|
345
|
+
* `.exit` + `kill -0` in a single ssh call over the shared ControlMaster socket.
|
|
346
|
+
*/
|
|
347
|
+
prefetchRemoteStatus(taskName: string): Promise<void>;
|
|
255
348
|
/**
|
|
256
349
|
* Fire any pending teammates in the given team whose `after` deps have all
|
|
257
350
|
* completed. Returns the list of teammates just launched. Repeatable:
|
|
@@ -266,6 +359,20 @@ export declare class AgentManager {
|
|
|
266
359
|
* exec path (src/lib/exec.ts). The team runner just supplies prompt + mode
|
|
267
360
|
* and reads stream-json events off stdout.
|
|
268
361
|
*/
|
|
362
|
+
/**
|
|
363
|
+
* Build the `agents run …` argv AFTER the `agents` binary — the flags + prompt
|
|
364
|
+
* scaffolding shared by the LOCAL launch (buildCommand, which prefixes
|
|
365
|
+
* process.execPath + the agents CLI path) and the REMOTE launch
|
|
366
|
+
* (launchRemoteProcess, which prefixes `agents` on the host via dispatch). Kept
|
|
367
|
+
* in one place so the PROMPT_SUFFIX / CLAUDE_PLAN_MODE_PREFIX scaffolding and the
|
|
368
|
+
* flag set can never drift between the two backends.
|
|
369
|
+
*
|
|
370
|
+
* `cwd` is intentionally NOT emitted here: the local path passes it as
|
|
371
|
+
* `--cwd`/`--add-dir` (below), while the remote path `cd`s into the host cwd
|
|
372
|
+
* before invoking `agents`. `sessionId` is likewise local-only (the remote run
|
|
373
|
+
* mints its own session on the host).
|
|
374
|
+
*/
|
|
375
|
+
private buildRunArgv;
|
|
269
376
|
private buildCommand;
|
|
270
377
|
get(agentId: string): Promise<AgentProcess | null>;
|
|
271
378
|
/**
|