@phnx-labs/agents-cli 1.22.24 → 1.22.26
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 +350 -1
- package/README.md +31 -6
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/browser.js +35 -0
- package/dist/commands/cloud.js +9 -5
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +108 -9
- package/dist/commands/exec.js +19 -16
- package/dist/commands/feed.d.ts +5 -0
- package/dist/commands/feed.js +21 -1
- package/dist/commands/focus.js +2 -2
- package/dist/commands/insights.d.ts +25 -19
- package/dist/commands/insights.js +107 -33
- package/dist/commands/menubar.js +8 -0
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/routines.d.ts +3 -0
- package/dist/commands/routines.js +72 -49
- package/dist/commands/run-cloud.js +1 -1
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- package/dist/commands/sessions-browser.d.ts +1 -1
- package/dist/commands/sessions-browser.js +27 -7
- package/dist/commands/sessions-resume.js +3 -2
- package/dist/commands/sessions.d.ts +13 -1
- package/dist/commands/sessions.js +28 -2
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup-watchdog.js +5 -10
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +106 -47
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +46 -2
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +26 -66
- package/dist/index.js +37 -2
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +155 -23
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- package/dist/lib/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- package/dist/lib/cloud/cursor.d.ts +79 -0
- package/dist/lib/cloud/cursor.js +228 -0
- package/dist/lib/cloud/registry.js +2 -0
- package/dist/lib/cloud/types.d.ts +7 -2
- package/dist/lib/cloud/types.js +14 -0
- package/dist/lib/crabbox/cli.d.ts +2 -2
- package/dist/lib/crabbox/config.d.ts +7 -8
- package/dist/lib/crabbox/config.js +14 -14
- package/dist/lib/crabbox/lease.d.ts +11 -4
- package/dist/lib/crabbox/lease.js +40 -8
- package/dist/lib/crabbox/setup-copy.d.ts +5 -0
- package/dist/lib/crabbox/setup-copy.js +17 -1
- package/dist/lib/daemon.js +27 -1
- package/dist/lib/device-config.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +13 -1
- package/dist/lib/devices/doctor-findings.js +73 -2
- package/dist/lib/events.d.ts +9 -0
- package/dist/lib/events.js +58 -0
- package/dist/lib/exec.d.ts +3 -3
- package/dist/lib/exec.js +24 -10
- package/dist/lib/feed-outcome.d.ts +3 -0
- package/dist/lib/feed-outcome.js +18 -10
- package/dist/lib/feed.d.ts +4 -0
- package/dist/lib/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +183 -6
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hosts/passthrough.d.ts +44 -0
- package/dist/lib/hosts/passthrough.js +84 -12
- package/dist/lib/hosts/ready.d.ts +2 -0
- package/dist/lib/hosts/ready.js +10 -1
- package/dist/lib/hosts/reconnect.d.ts +14 -12
- package/dist/lib/hosts/reconnect.js +41 -40
- package/dist/lib/mcp.js +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/snapshot.d.ts +15 -0
- package/dist/lib/menubar/snapshot.js +40 -0
- package/dist/lib/plugins.js +13 -1
- package/dist/lib/resources/mcp.js +3 -0
- package/dist/lib/routine-process-cleanup.d.ts +9 -0
- package/dist/lib/routine-process-cleanup.js +73 -0
- package/dist/lib/routines.js +14 -2
- package/dist/lib/runner.d.ts +0 -3
- package/dist/lib/runner.js +6 -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/bundles.js +1 -20
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +13 -0
- package/dist/lib/secrets/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
- package/dist/lib/secrets/rc-hygiene.js +14 -1
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/session/active.d.ts +4 -0
- package/dist/lib/session/db.d.ts +21 -3
- package/dist/lib/session/db.js +229 -15
- package/dist/lib/session/discover.d.ts +1 -0
- package/dist/lib/session/discover.js +115 -19
- package/dist/lib/session/insights.d.ts +18 -0
- package/dist/lib/session/insights.js +143 -1
- package/dist/lib/session/remote-list.d.ts +2 -0
- package/dist/lib/session/remote-list.js +1 -0
- package/dist/lib/session/session-cache.d.ts +4 -4
- package/dist/lib/session/session-cache.js +4 -4
- package/dist/lib/session/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- package/dist/lib/shims.js +21 -1
- package/dist/lib/signin-badge.js +2 -0
- package/dist/lib/ssh-exec.js +8 -2
- package/dist/lib/startup/command-registry.d.ts +16 -0
- package/dist/lib/startup/command-registry.js +46 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +76 -8
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/versions.js +16 -1
- package/dist/lib/watchdog/service.d.ts +17 -0
- package/dist/lib/watchdog/service.js +39 -0
- package/package.json +1 -1
|
@@ -28,11 +28,17 @@ export function toolEvidenceSourcePath(filePath, agent) {
|
|
|
28
28
|
return filePath;
|
|
29
29
|
}
|
|
30
30
|
function deleteSessionCalls(db, sessionId) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
const rows = db.prepare(`SELECT rowid, call_key FROM tool_calls WHERE session_id = ?`)
|
|
32
|
+
.all(sessionId);
|
|
33
|
+
const deletePrograms = db.prepare(`DELETE FROM tool_call_programs WHERE call_key = ?`);
|
|
34
|
+
const deleteOccurrences = db.prepare(`DELETE FROM tool_program_occurrences WHERE call_key = ?`);
|
|
35
|
+
// Addressed by rowid, never by the UNINDEXED call_key — see the tool_call_text
|
|
36
|
+
// schema comment in db.ts. A call_key predicate scans the whole FTS index.
|
|
37
|
+
const deleteText = db.prepare(`DELETE FROM tool_call_text WHERE rowid = ?`);
|
|
38
|
+
for (const { rowid, call_key } of rows) {
|
|
39
|
+
deletePrograms.run(call_key);
|
|
40
|
+
deleteOccurrences.run(call_key);
|
|
41
|
+
deleteText.run(rowid);
|
|
36
42
|
}
|
|
37
43
|
db.prepare(`DELETE FROM tool_calls WHERE session_id = ?`).run(sessionId);
|
|
38
44
|
}
|
|
@@ -61,8 +67,11 @@ export function purgeMissingToolCallsInDirectory(db, dirPath, currentFilePaths)
|
|
|
61
67
|
}
|
|
62
68
|
return purged;
|
|
63
69
|
}
|
|
64
|
-
/** Persist one parser batch
|
|
65
|
-
export function persistToolCalls(db, session, calls, sourceStamp,
|
|
70
|
+
/** Persist one parser batch, its file stamp, and its resume point atomically. */
|
|
71
|
+
export function persistToolCalls(db, session, calls, sourceStamp, options = {}) {
|
|
72
|
+
const mode = options.mode ?? 'replace';
|
|
73
|
+
const maxSessionBytes = options.maxSessionBytes ?? TOOL_SESSION_EVIDENCE_MAX_BYTES;
|
|
74
|
+
const resume = options.resume ?? null;
|
|
66
75
|
const sourcePath = toolEvidenceSourcePath(session.filePath, session.agent);
|
|
67
76
|
const insertCall = db.prepare(`
|
|
68
77
|
INSERT INTO tool_calls (
|
|
@@ -89,16 +98,19 @@ export function persistToolCalls(db, session, calls, sourceStamp, mode = 'replac
|
|
|
89
98
|
INSERT INTO tool_program_occurrences (call_key, occurrence_ordinal, program, role)
|
|
90
99
|
VALUES (?, ?, ?, ?)
|
|
91
100
|
`);
|
|
92
|
-
|
|
101
|
+
// tool_call_text rows are addressed by the rowid of the tool_calls row they
|
|
102
|
+
// describe (db.ts schema comment): its UNINDEXED call_key cannot be seeked.
|
|
103
|
+
const insertText = db.prepare(`INSERT INTO tool_call_text (rowid, call_key, tool, input, output, error) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
104
|
+
const callRowid = db.prepare(`SELECT rowid FROM tool_calls WHERE call_key = ?`);
|
|
93
105
|
const deletePrograms = db.prepare(`DELETE FROM tool_call_programs WHERE call_key = ?`);
|
|
94
106
|
const deleteOccurrences = db.prepare(`DELETE FROM tool_program_occurrences WHERE call_key = ?`);
|
|
95
|
-
const deleteText = db.prepare(`DELETE FROM tool_call_text WHERE
|
|
107
|
+
const deleteText = db.prepare(`DELETE FROM tool_call_text WHERE rowid = ?`);
|
|
96
108
|
const deleteCall = db.prepare(`DELETE FROM tool_calls WHERE call_key = ?`);
|
|
97
109
|
const writeLedger = db.prepare(`
|
|
98
110
|
INSERT INTO tool_scan_ledger (
|
|
99
111
|
session_id, file_path, file_mtime_ms, file_size, extractor_version, indexed_at, call_count,
|
|
100
|
-
evidence_bytes
|
|
101
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
112
|
+
evidence_bytes, parser_state, parsed_offset
|
|
113
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
102
114
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
103
115
|
file_path = excluded.file_path,
|
|
104
116
|
file_mtime_ms = excluded.file_mtime_ms,
|
|
@@ -106,7 +118,9 @@ export function persistToolCalls(db, session, calls, sourceStamp, mode = 'replac
|
|
|
106
118
|
extractor_version = excluded.extractor_version,
|
|
107
119
|
indexed_at = excluded.indexed_at,
|
|
108
120
|
call_count = excluded.call_count,
|
|
109
|
-
evidence_bytes = excluded.evidence_bytes
|
|
121
|
+
evidence_bytes = excluded.evidence_bytes,
|
|
122
|
+
parser_state = excluded.parser_state,
|
|
123
|
+
parsed_offset = excluded.parsed_offset
|
|
110
124
|
`);
|
|
111
125
|
const txn = db.transaction(() => {
|
|
112
126
|
if (mode === 'replace')
|
|
@@ -119,7 +133,7 @@ export function persistToolCalls(db, session, calls, sourceStamp, mode = 'replac
|
|
|
119
133
|
throw new Error('Append tool evidence requires an existing tool ledger row.');
|
|
120
134
|
}
|
|
121
135
|
if (mode === 'append' && calls.length === 0) {
|
|
122
|
-
writeLedger.run(session.id, ledgerPath, sourceStamp.fileMtimeMs, sourceStamp.fileSize, TOOL_INDEX_VERSION, Date.now(), priorLedger.call_count, priorLedger.evidence_bytes);
|
|
136
|
+
writeLedger.run(session.id, ledgerPath, sourceStamp.fileMtimeMs, sourceStamp.fileSize, TOOL_INDEX_VERSION, Date.now(), priorLedger.call_count, priorLedger.evidence_bytes, resume?.parserState ?? null, resume?.parsedOffset ?? null);
|
|
123
137
|
return;
|
|
124
138
|
}
|
|
125
139
|
const limitKey = toolCallKey(session.id, TOOL_INDEX_LIMIT_ORDINAL);
|
|
@@ -129,9 +143,11 @@ export function persistToolCalls(db, session, calls, sourceStamp, mode = 'replac
|
|
|
129
143
|
const priorLimit = mode === 'append'
|
|
130
144
|
? existingSize.get(session.id, TOOL_INDEX_LIMIT_ORDINAL)
|
|
131
145
|
: undefined;
|
|
146
|
+
const limitRow = callRowid.get(limitKey);
|
|
132
147
|
deletePrograms.run(limitKey);
|
|
133
148
|
deleteOccurrences.run(limitKey);
|
|
134
|
-
|
|
149
|
+
if (limitRow)
|
|
150
|
+
deleteText.run(limitRow.rowid);
|
|
135
151
|
deleteCall.run(limitKey);
|
|
136
152
|
const existingRows = mode === 'append'
|
|
137
153
|
? calls.map((call) => ({
|
|
@@ -182,17 +198,20 @@ export function persistToolCalls(db, session, calls, sourceStamp, mode = 'replac
|
|
|
182
198
|
for (const call of accepted) {
|
|
183
199
|
const key = toolCallKey(session.id, call.ordinal);
|
|
184
200
|
insertCall.run(key, session.id, call.ordinal, call.sourceCallId ?? null, call.timestamp, call.tool, call.input, call.outcome, call.exitCode ?? null, call.statusCode ?? null, call.errorCode ?? null, call.output ?? null, call.error ?? null, call.parseError ?? null, toolCallEvidenceBytes(call));
|
|
201
|
+
// The upsert above preserves the rowid of a call it updated, so this is
|
|
202
|
+
// the same rowid the existing text row (if any) was written under.
|
|
203
|
+
const { rowid } = callRowid.get(key);
|
|
185
204
|
deletePrograms.run(key);
|
|
186
205
|
deleteOccurrences.run(key);
|
|
187
|
-
deleteText.run(
|
|
206
|
+
deleteText.run(rowid);
|
|
188
207
|
for (const program of call.programs)
|
|
189
208
|
insertProgram.run(key, program);
|
|
190
209
|
call.programOccurrences.forEach((occurrence, occurrenceOrdinal) => {
|
|
191
210
|
insertOccurrence.run(key, occurrenceOrdinal, occurrence.program, occurrence.role);
|
|
192
211
|
});
|
|
193
|
-
insertText.run(key, call.tool, call.input, call.output ?? '', call.error ?? '');
|
|
212
|
+
insertText.run(rowid, key, call.tool, call.input, call.output ?? '', call.error ?? '');
|
|
194
213
|
}
|
|
195
|
-
writeLedger.run(session.id, ledgerPath, sourceStamp.fileMtimeMs, sourceStamp.fileSize, TOOL_INDEX_VERSION, Date.now(), count, storedBytes);
|
|
214
|
+
writeLedger.run(session.id, ledgerPath, sourceStamp.fileMtimeMs, sourceStamp.fileSize, TOOL_INDEX_VERSION, Date.now(), count, storedBytes, resume?.parserState ?? null, resume?.parsedOffset ?? null);
|
|
196
215
|
});
|
|
197
216
|
txn();
|
|
198
217
|
}
|
package/dist/lib/shims.js
CHANGED
|
@@ -584,6 +584,18 @@ elif [ "$AGENT" = "muse" ]; then
|
|
|
584
584
|
esac
|
|
585
585
|
fi
|
|
586
586
|
fi
|
|
587
|
+
elif [ "$AGENT" = "warp" ]; then
|
|
588
|
+
# Warp Agent CLI installs a global, self-updating oz binary (brew cask on
|
|
589
|
+
# macOS, the oz-stable apt|yum|pacman package on Linux) -- a platform/package
|
|
590
|
+
# specific location, not ~/.local/bin -- so resolve it from PATH with the same
|
|
591
|
+
# shims-dir re-exec guard as droid/muse.
|
|
592
|
+
BINARY=$(adopted_original_bin || echo "")
|
|
593
|
+
if [ -z "$BINARY" ]; then
|
|
594
|
+
BINARY=$(command -v oz 2>/dev/null || echo "")
|
|
595
|
+
case "$(readlink -f "$BINARY" 2>/dev/null)" in
|
|
596
|
+
"$AGENTS_USER_DIR/.cache/shims/"*) BINARY="" ;;
|
|
597
|
+
esac
|
|
598
|
+
fi
|
|
587
599
|
else
|
|
588
600
|
BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
|
|
589
601
|
fi
|
|
@@ -1041,7 +1053,15 @@ else
|
|
|
1041
1053
|
"$HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
1042
1054
|
esac
|
|
1043
1055
|
fi`
|
|
1044
|
-
:
|
|
1056
|
+
: agent === 'warp'
|
|
1057
|
+
? `# Warp Agent CLI installs a global self-updating \`oz\` binary (brew cask on
|
|
1058
|
+
# macOS, oz-stable apt|yum|pacman on Linux) — a platform-specific location, not
|
|
1059
|
+
# ~/.local/bin — so resolve it from PATH, refusing anything under our shims dir.
|
|
1060
|
+
BINARY=$(command -v oz 2>/dev/null || echo "")
|
|
1061
|
+
case "$BINARY" in
|
|
1062
|
+
"$HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
1063
|
+
esac`
|
|
1064
|
+
: `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
|
|
1045
1065
|
return `#!/bin/bash
|
|
1046
1066
|
# Auto-generated by agents-cli - do not edit
|
|
1047
1067
|
# ${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}
|
package/dist/lib/signin-badge.js
CHANGED
|
@@ -24,6 +24,8 @@ export function loginHint(agentId) {
|
|
|
24
24
|
return `${cli}, then /login`;
|
|
25
25
|
case 'codex':
|
|
26
26
|
case 'grok':
|
|
27
|
+
// Warp Agent CLI: `oz login` opens a browser sign-in (or set WARP_API_KEY).
|
|
28
|
+
case 'warp':
|
|
27
29
|
return `${cli} login`;
|
|
28
30
|
case 'opencode':
|
|
29
31
|
return `${cli} auth login`;
|
package/dist/lib/ssh-exec.js
CHANGED
|
@@ -117,7 +117,11 @@ export function sshExec(target, remoteCmd, opts = {}) {
|
|
|
117
117
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
118
|
windowsHide: true,
|
|
119
119
|
});
|
|
120
|
-
|
|
120
|
+
// Node's spawnSync with a `timeout` option kills the child via SIGTERM and sets
|
|
121
|
+
// res.signal (e.g. 'SIGTERM') — it does NOT set res.error.code = 'ETIMEDOUT'.
|
|
122
|
+
// Check both so the detection fires whether the timeout is signalled or errored.
|
|
123
|
+
const timedOut = !!(res.error && res.error.code === 'ETIMEDOUT') ||
|
|
124
|
+
(!!opts.timeoutMs && res.signal !== null);
|
|
121
125
|
return {
|
|
122
126
|
code: typeof res.status === 'number' ? res.status : null,
|
|
123
127
|
stdout: res.stdout ?? '',
|
|
@@ -206,7 +210,9 @@ export function sshExecRaw(target, remoteCmd, opts = {}) {
|
|
|
206
210
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
207
211
|
windowsHide: true,
|
|
208
212
|
});
|
|
209
|
-
|
|
213
|
+
// Same fix as sshExec: spawnSync sets res.signal, not error.code = 'ETIMEDOUT'.
|
|
214
|
+
const timedOut = !!(res.error && res.error.code === 'ETIMEDOUT') ||
|
|
215
|
+
(!!opts.timeoutMs && res.signal !== null);
|
|
210
216
|
return {
|
|
211
217
|
code: typeof res.status === 'number' ? res.status : null,
|
|
212
218
|
stdout: res.stdout ?? Buffer.alloc(0),
|
|
@@ -47,6 +47,7 @@ export declare const loadMonitors: ModuleLoader;
|
|
|
47
47
|
export declare const loadProjects: ModuleLoader;
|
|
48
48
|
export declare const loadRun: ModuleLoader;
|
|
49
49
|
export declare const loadResume: ModuleLoader;
|
|
50
|
+
export declare const loadReconnect: ModuleLoader;
|
|
50
51
|
export declare const loadFork: ModuleLoader;
|
|
51
52
|
export declare const loadDefaults: ModuleLoader;
|
|
52
53
|
export declare const loadSet: ModuleLoader;
|
|
@@ -131,3 +132,18 @@ export declare const LAZY_COMMAND_NAMES: ReadonlySet<string>;
|
|
|
131
132
|
* are handled directly in src/index.ts.
|
|
132
133
|
*/
|
|
133
134
|
export declare const COMMAND_LOADERS: Record<string, ModuleLoader[]>;
|
|
135
|
+
/**
|
|
136
|
+
* Every top-level command name the CLI answers to — the loader table plus the
|
|
137
|
+
* inline aliases/tombstones above. This is the "does this command exist?"
|
|
138
|
+
* predicate for code that runs BEFORE commander parses, most importantly the
|
|
139
|
+
* `--host`/`--device` router (lib/hosts/passthrough.ts): without it a typo'd
|
|
140
|
+
* command carrying `--host` reported a flag-support error instead of
|
|
141
|
+
* `unknown command` (RUSH-2022).
|
|
142
|
+
*
|
|
143
|
+
* Commander sub-aliases (`sessions ls`, `teams rm`, …) are deliberately absent —
|
|
144
|
+
* this set is top-level only. `command-registry.test.ts` pins it against the real
|
|
145
|
+
* registered command tree so a new command can never drift out of it.
|
|
146
|
+
*/
|
|
147
|
+
export declare const KNOWN_TOP_LEVEL_COMMANDS: ReadonlySet<string>;
|
|
148
|
+
/** Whether `name` is a top-level command this CLI registers. See {@link KNOWN_TOP_LEVEL_COMMANDS}. */
|
|
149
|
+
export declare function isKnownTopLevelCommand(name: string): boolean;
|
|
@@ -25,6 +25,7 @@ export const loadMonitors = async () => (await import('../../commands/monitors.j
|
|
|
25
25
|
export const loadProjects = async () => (await import('../../commands/projects.js')).registerProjectsCommands;
|
|
26
26
|
export const loadRun = async () => (await import('../../commands/exec.js')).registerRunCommand;
|
|
27
27
|
export const loadResume = async () => (await import('../../commands/resume.js')).registerResumeCommand;
|
|
28
|
+
export const loadReconnect = async () => (await import('../../commands/reconnect.js')).registerReconnectCommand;
|
|
28
29
|
export const loadFork = async () => (await import('../../commands/fork.js')).registerForkCommand;
|
|
29
30
|
export const loadDefaults = async () => (await import('../../commands/defaults.js')).registerDefaultsCommands;
|
|
30
31
|
export const loadSet = async () => (await import('../../commands/set.js')).registerSetCommand;
|
|
@@ -53,6 +54,7 @@ export const loadUsage = async () => (await import('../../commands/usage.js')).r
|
|
|
53
54
|
export const loadCost = async () => (await import('../../commands/cost.js')).registerCostCommand;
|
|
54
55
|
export const loadInsights = async () => (await import('../../commands/insights.js')).registerInsightsCommand;
|
|
55
56
|
export const loadPerf = async () => (await import('../../commands/perf.js')).registerPerfCommand;
|
|
57
|
+
// Thin deprecated alias of `agents insights mix` — no second mix implementation.
|
|
56
58
|
export const loadTrends = async () => (await import('../../commands/trends.js')).registerTrendsCommand;
|
|
57
59
|
export const loadOutput = async () => (await import('../../commands/output.js')).registerOutputCommand;
|
|
58
60
|
export const loadBudget = async () => (await import('../../commands/budget.js')).registerBudgetCommand;
|
|
@@ -98,6 +100,7 @@ export const loadHumans = async () => (await import('../../commands/humans.js'))
|
|
|
98
100
|
export const LAZY_COMMAND_NAMES = new Set([
|
|
99
101
|
'sessions',
|
|
100
102
|
'resume',
|
|
103
|
+
'reconnect',
|
|
101
104
|
'roster',
|
|
102
105
|
'teams',
|
|
103
106
|
'cloud',
|
|
@@ -146,11 +149,17 @@ export const COMMAND_LOADERS = {
|
|
|
146
149
|
registry: [loadPackages],
|
|
147
150
|
search: [loadPackages],
|
|
148
151
|
install: [loadPackages],
|
|
152
|
+
// packages.ts also registers `publish` at top level (commands/packages.ts:435).
|
|
153
|
+
// It was missing here, so `agents publish` only worked via the unknown-command
|
|
154
|
+
// fallback that registers the whole tree — and the --host router could not see
|
|
155
|
+
// it as a real command at all (RUSH-2022).
|
|
156
|
+
publish: [loadPackages],
|
|
149
157
|
routines: [loadRoutines],
|
|
150
158
|
monitors: [loadMonitors],
|
|
151
159
|
projects: [loadProjects],
|
|
152
160
|
run: [loadRun],
|
|
153
161
|
resume: [loadResume],
|
|
162
|
+
reconnect: [loadReconnect],
|
|
154
163
|
fork: [loadFork],
|
|
155
164
|
defaults: [loadDefaults],
|
|
156
165
|
set: [loadSet],
|
|
@@ -231,3 +240,40 @@ export const COMMAND_LOADERS = {
|
|
|
231
240
|
funnel: [loadFunnel],
|
|
232
241
|
humans: [loadHumans],
|
|
233
242
|
};
|
|
243
|
+
/**
|
|
244
|
+
* Top-level names that {@link COMMAND_LOADERS} does not carry because they are
|
|
245
|
+
* registered inline in src/index.ts — closures over entry-point state (the
|
|
246
|
+
* deprecated aliases and tombstones) plus the internal/upgrade commands. They are
|
|
247
|
+
* real commands, so anything that asks "does this command exist?" must count them.
|
|
248
|
+
*/
|
|
249
|
+
const INLINE_COMMAND_NAMES = [
|
|
250
|
+
'perms', // deprecated alias -> permissions
|
|
251
|
+
'exec', // deprecated alias -> run
|
|
252
|
+
'jobs', // deprecated alias -> routines
|
|
253
|
+
'cron', // deprecated alias -> routines
|
|
254
|
+
'check', // tombstone -> doctor --check
|
|
255
|
+
'resources', // tombstone -> view --merged
|
|
256
|
+
'hq', // tombstone
|
|
257
|
+
'_internal',
|
|
258
|
+
'upgrade',
|
|
259
|
+
];
|
|
260
|
+
/**
|
|
261
|
+
* Every top-level command name the CLI answers to — the loader table plus the
|
|
262
|
+
* inline aliases/tombstones above. This is the "does this command exist?"
|
|
263
|
+
* predicate for code that runs BEFORE commander parses, most importantly the
|
|
264
|
+
* `--host`/`--device` router (lib/hosts/passthrough.ts): without it a typo'd
|
|
265
|
+
* command carrying `--host` reported a flag-support error instead of
|
|
266
|
+
* `unknown command` (RUSH-2022).
|
|
267
|
+
*
|
|
268
|
+
* Commander sub-aliases (`sessions ls`, `teams rm`, …) are deliberately absent —
|
|
269
|
+
* this set is top-level only. `command-registry.test.ts` pins it against the real
|
|
270
|
+
* registered command tree so a new command can never drift out of it.
|
|
271
|
+
*/
|
|
272
|
+
export const KNOWN_TOP_LEVEL_COMMANDS = new Set([
|
|
273
|
+
...Object.keys(COMMAND_LOADERS),
|
|
274
|
+
...INLINE_COMMAND_NAMES,
|
|
275
|
+
]);
|
|
276
|
+
/** Whether `name` is a top-level command this CLI registers. See {@link KNOWN_TOP_LEVEL_COMMANDS}. */
|
|
277
|
+
export function isKnownTopLevelCommand(name) {
|
|
278
|
+
return KNOWN_TOP_LEVEL_COMMANDS.has(name);
|
|
279
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { captureProcessStartTime } from '../platform/index.js';
|
|
2
2
|
import { AgentType } from './parsers.js';
|
|
3
|
+
import { NoViableDeviceError } from './scheduler.js';
|
|
3
4
|
/**
|
|
4
5
|
* Compute the Lowest Common Ancestor (LCA) of multiple file paths.
|
|
5
6
|
* Returns the deepest common directory shared by all paths.
|
|
@@ -405,6 +406,18 @@ export declare class AgentManager {
|
|
|
405
406
|
* teammate schedules identically no matter how it was fired.
|
|
406
407
|
*/
|
|
407
408
|
private maybeSchedulePlacement;
|
|
409
|
+
/** Human label of a teammate's agent for the placement fail-loud message. */
|
|
410
|
+
private placementAgentLabel;
|
|
411
|
+
/**
|
|
412
|
+
* Pre-flight the team pool at `teams start` (RUSH-2002): probe the pool and
|
|
413
|
+
* confirm each distinct agent among the pending, unpinned, pooled teammates
|
|
414
|
+
* has at least one viable device. Returns the first {@link NoViableDeviceError}
|
|
415
|
+
* so the command can fail loud BEFORE launching a wave, rather than silently
|
|
416
|
+
* leaving teammates stranded pending. A poolless team (no `--devices`) or a
|
|
417
|
+
* probe that could not gather positive evidence returns null — fail-loud fires
|
|
418
|
+
* only on proof that no device can run the agent, never on a probe miss.
|
|
419
|
+
*/
|
|
420
|
+
preflightPlacement(taskName: string): Promise<NoViableDeviceError | null>;
|
|
408
421
|
/**
|
|
409
422
|
* One-ssh-per-host batched liveness/exit pre-pass for a team's remote teammates.
|
|
410
423
|
* The supervisor calls this each wave BEFORE listByTask() so the per-teammate
|
package/dist/lib/teams/agents.js
CHANGED
|
@@ -34,7 +34,8 @@ import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
|
34
34
|
import { pullRemoteLogDelta, REMOTE_MIRROR_MAX_BYTES } from '../hosts/progress.js';
|
|
35
35
|
import { createRemoteWorktree, ensureRemoteRepo } from './remoteWorktree.js';
|
|
36
36
|
import { getTeam } from './registry.js';
|
|
37
|
-
import { resolvePlacement,
|
|
37
|
+
import { resolvePlacement, classifyExclusions, NoViableDeviceError } from './scheduler.js';
|
|
38
|
+
import { probePoolSignals } from './placement-probe.js';
|
|
38
39
|
import { readMaxConcurrentCaps } from '../device-config.js';
|
|
39
40
|
import chalk from 'chalk';
|
|
40
41
|
let lastMemoryWarnAt = 0;
|
|
@@ -197,7 +198,7 @@ export function buildTeammateSpawnEnv(envOverrides) {
|
|
|
197
198
|
*/
|
|
198
199
|
export { captureProcessStartTime };
|
|
199
200
|
/** Agent types the team runner supports. */
|
|
200
|
-
const TEAM_AGENT_TYPES = ['codex', 'cursor', 'claude', 'opencode', 'grok', 'antigravity', 'kimi', 'droid'];
|
|
201
|
+
const TEAM_AGENT_TYPES = ['codex', 'cursor', 'claude', 'opencode', 'grok', 'antigravity', 'kimi', 'droid', 'warp'];
|
|
201
202
|
// Suffix appended to all prompts to ensure agents provide a summary
|
|
202
203
|
const PROMPT_SUFFIX = `
|
|
203
204
|
|
|
@@ -1756,7 +1757,7 @@ export class AgentManager {
|
|
|
1756
1757
|
* (immediate add-launch) and startReady() (staged launch) so an unpinned pool
|
|
1757
1758
|
* teammate schedules identically no matter how it was fired.
|
|
1758
1759
|
*/
|
|
1759
|
-
async maybeSchedulePlacement(agent, taskName) {
|
|
1760
|
+
async maybeSchedulePlacement(agent, taskName, opts = {}) {
|
|
1760
1761
|
if (agent.hostName || agent.cloudProvider)
|
|
1761
1762
|
return;
|
|
1762
1763
|
const teamMeta = await getTeam(taskName);
|
|
@@ -1765,15 +1766,79 @@ export class AgentManager {
|
|
|
1765
1766
|
const roster = await this.listByTask(taskName);
|
|
1766
1767
|
const pool = teamMeta.devices ?? [];
|
|
1767
1768
|
const maxConcurrent = pool.length > 1 ? readMaxConcurrentCaps(pool) : undefined;
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1769
|
+
// On the start path (opts.probe), gather live signals so the pick is health-,
|
|
1770
|
+
// harness-, and load-aware (RUSH-2002); the add path stays the cap-only
|
|
1771
|
+
// roster count so `teams add` never blocks on an SSH fan-out. Cached per
|
|
1772
|
+
// (pool, agent), so a wave placing many teammates probes the pool once.
|
|
1773
|
+
const signals = opts.probe && pool.length > 0
|
|
1774
|
+
? await probePoolSignals(pool, agent.agentType, { now: Date.now() })
|
|
1775
|
+
: undefined;
|
|
1776
|
+
const placeOpts = {
|
|
1777
|
+
maxConcurrent,
|
|
1778
|
+
signals,
|
|
1779
|
+
agentLabel: this.placementAgentLabel(agent),
|
|
1780
|
+
};
|
|
1781
|
+
if (signals) {
|
|
1782
|
+
for (const e of classifyExclusions(pool, roster, placeOpts).excluded) {
|
|
1783
|
+
const why = e.reason === 'capped'
|
|
1784
|
+
? `at its agents.max-concurrent cap (${e.detail} running)`
|
|
1785
|
+
: e.reason === 'not-installed'
|
|
1786
|
+
? `does not have ${this.placementAgentLabel(agent)} installed`
|
|
1787
|
+
: e.reason;
|
|
1788
|
+
console.error(chalk.dim(`[placement] '${e.device}' excluded from auto-pick — ${why}`));
|
|
1771
1789
|
}
|
|
1772
1790
|
}
|
|
1773
|
-
const { device } = resolvePlacement(teamMeta, null, roster,
|
|
1791
|
+
const { device } = resolvePlacement(teamMeta, null, roster, placeOpts);
|
|
1774
1792
|
if (device)
|
|
1775
1793
|
await this.resolveScheduledPlacement(agent, device, taskName);
|
|
1776
1794
|
}
|
|
1795
|
+
/** Human label of a teammate's agent for the placement fail-loud message. */
|
|
1796
|
+
placementAgentLabel(agent) {
|
|
1797
|
+
return agent.version ? `${agent.agentType}@${agent.version}` : String(agent.agentType);
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Pre-flight the team pool at `teams start` (RUSH-2002): probe the pool and
|
|
1801
|
+
* confirm each distinct agent among the pending, unpinned, pooled teammates
|
|
1802
|
+
* has at least one viable device. Returns the first {@link NoViableDeviceError}
|
|
1803
|
+
* so the command can fail loud BEFORE launching a wave, rather than silently
|
|
1804
|
+
* leaving teammates stranded pending. A poolless team (no `--devices`) or a
|
|
1805
|
+
* probe that could not gather positive evidence returns null — fail-loud fires
|
|
1806
|
+
* only on proof that no device can run the agent, never on a probe miss.
|
|
1807
|
+
*/
|
|
1808
|
+
async preflightPlacement(taskName) {
|
|
1809
|
+
await this.initialize();
|
|
1810
|
+
const teamMeta = await getTeam(taskName);
|
|
1811
|
+
const pool = teamMeta?.devices ?? [];
|
|
1812
|
+
if (!teamMeta || pool.length === 0)
|
|
1813
|
+
return null;
|
|
1814
|
+
const roster = await this.listByTask(taskName);
|
|
1815
|
+
const pending = roster.filter((a) => a.status === AgentStatus.PENDING && !a.hostName && !a.cloudProvider);
|
|
1816
|
+
if (pending.length === 0)
|
|
1817
|
+
return null;
|
|
1818
|
+
const maxConcurrent = pool.length > 1 ? readMaxConcurrentCaps(pool) : undefined;
|
|
1819
|
+
// One representative teammate per distinct agent type — the signal + gate is
|
|
1820
|
+
// per harness, not per teammate.
|
|
1821
|
+
const byAgent = new Map();
|
|
1822
|
+
for (const a of pending)
|
|
1823
|
+
if (!byAgent.has(a.agentType))
|
|
1824
|
+
byAgent.set(a.agentType, a);
|
|
1825
|
+
for (const [, rep] of byAgent) {
|
|
1826
|
+
const signals = await probePoolSignals(pool, rep.agentType, { now: Date.now() });
|
|
1827
|
+
try {
|
|
1828
|
+
resolvePlacement(teamMeta, null, roster, {
|
|
1829
|
+
maxConcurrent,
|
|
1830
|
+
signals,
|
|
1831
|
+
agentLabel: this.placementAgentLabel(rep),
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
catch (err) {
|
|
1835
|
+
if (err instanceof NoViableDeviceError)
|
|
1836
|
+
return err;
|
|
1837
|
+
throw err;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1777
1842
|
/**
|
|
1778
1843
|
* One-ssh-per-host batched liveness/exit pre-pass for a team's remote teammates.
|
|
1779
1844
|
* The supervisor calls this each wave BEFORE listByTask() so the per-teammate
|
|
@@ -1866,9 +1931,12 @@ export class AgentManager {
|
|
|
1866
1931
|
// immediate-add and staged paths agree. A null pick keeps hostName null →
|
|
1867
1932
|
// local spawn, unchanged. Cloud teammates never schedule.
|
|
1868
1933
|
try {
|
|
1869
|
-
await this.maybeSchedulePlacement(agent, taskName);
|
|
1934
|
+
await this.maybeSchedulePlacement(agent, taskName, { probe: true });
|
|
1870
1935
|
}
|
|
1871
1936
|
catch (err) {
|
|
1937
|
+
// A NoViableDeviceError means the pool cannot host this teammate right
|
|
1938
|
+
// now — leave it PENDING (never a silent local fallback) and surface the
|
|
1939
|
+
// reason; a transient device loss is retried next wave.
|
|
1872
1940
|
console.error(`Could not schedule ${agent.agentId} onto the team pool:`, err);
|
|
1873
1941
|
continue;
|
|
1874
1942
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Supported agent CLI types for team spawning. */
|
|
2
|
-
export type AgentType = 'codex' | 'gemini' | 'cursor' | 'claude' | 'opencode' | 'grok' | 'antigravity' | 'kimi' | 'droid';
|
|
2
|
+
export type AgentType = 'codex' | 'gemini' | 'cursor' | 'claude' | 'opencode' | 'grok' | 'antigravity' | 'kimi' | 'droid' | 'warp';
|
|
3
3
|
/** Normalize a raw JSON event from any agent type into an array of unified event objects. */
|
|
4
4
|
export declare function normalizeEvents(agentType: AgentType, raw: any): any[];
|
|
5
5
|
/** Normalize a raw JSON event, returning only the first unified event (convenience wrapper). */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type AgentType } from './agents.js';
|
|
2
|
+
import type { DevicePlacementSignal } from './scheduler.js';
|
|
3
|
+
/** Per-remote readiness probe budget — matches the health probe's short window
|
|
4
|
+
* (`agents view` on a warm box is sub-second; a wedged one degrades to unknown). */
|
|
5
|
+
export declare const READY_PROBE_TIMEOUT_MS = 8000;
|
|
6
|
+
/** How long a probed pool snapshot is reused within a `teams start` wave. Short
|
|
7
|
+
* enough that a device coming online / going overloaded is seen next wave, long
|
|
8
|
+
* enough that placing a wave of teammates does not re-fan-out per teammate. */
|
|
9
|
+
export declare const SIGNAL_TTL_MS = 15000;
|
|
10
|
+
/** Clear the probe cache — for tests and after a device-registry change. */
|
|
11
|
+
export declare function clearPlacementSignalCache(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Probe every device in the team pool and return a name→signal map for the pure
|
|
14
|
+
* placement pick. Devices with no data at all are omitted (the pick then neither
|
|
15
|
+
* excludes nor prefers them). Never throws — a probe failure degrades to a
|
|
16
|
+
* missing/partial signal.
|
|
17
|
+
*/
|
|
18
|
+
export declare function probePoolSignals(pool: string[], agent: AgentType, opts?: {
|
|
19
|
+
force?: boolean;
|
|
20
|
+
now?: number;
|
|
21
|
+
}): Promise<Map<string, DevicePlacementSignal>>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live pool probing for the teams placement scheduler (RUSH-2002).
|
|
3
|
+
*
|
|
4
|
+
* Gathers one {@link DevicePlacementSignal} per pool device so the pure pick in
|
|
5
|
+
* {@link ./scheduler} can filter unreachable / overloaded / not-installed
|
|
6
|
+
* devices and rank the rest. Two concerns, both best-effort:
|
|
7
|
+
*
|
|
8
|
+
* - reachability + headroom + load ← {@link probeFleetStats} (one parallel
|
|
9
|
+
* SSH fan-out over the pool; the local box is measured directly).
|
|
10
|
+
* - requested agent installed (+ signed in, locally) ← a one-shot readiness
|
|
11
|
+
* probe per remote device ({@link buildReadyProbeCommand} → `agents view`),
|
|
12
|
+
* the local box via {@link checkCliAvailable}/{@link checkCliSignedIn}.
|
|
13
|
+
*
|
|
14
|
+
* The result is cached briefly per (pool, agent) so a `teams start` wave that
|
|
15
|
+
* places N teammates probes the pool ONCE, not N times — the roster-count part
|
|
16
|
+
* of the rank stays live (the pure pick recounts the roster each call), only the
|
|
17
|
+
* SSH-measured load/harness snapshot is reused within the TTL.
|
|
18
|
+
*
|
|
19
|
+
* All SSH here is via `execFile` (async, bounded) so the whole pool is probed in
|
|
20
|
+
* parallel; a slow or wedged box degrades to "no signal" instead of blocking the
|
|
21
|
+
* launch. Remote sign-in is deliberately left unknown — sign-in detection is
|
|
22
|
+
* unreliable over SSH (see {@link checkCliSignedIn}); it ranks the local box
|
|
23
|
+
* only, and the install gate (not sign-in) is what drives the fail-loud.
|
|
24
|
+
*/
|
|
25
|
+
import { execFile } from 'child_process';
|
|
26
|
+
import { probeFleetStats, headroom } from '../devices/health.js';
|
|
27
|
+
import { loadDevicesSync } from '../devices/registry.js';
|
|
28
|
+
import { buildSshInvocation, writeAskpassShim } from '../devices/connect.js';
|
|
29
|
+
import { buildReadyProbeCommand, parseReadyProbe, viewHasAgent } from '../hosts/ready.js';
|
|
30
|
+
import { localMachineId } from '../session/origin-machine.js';
|
|
31
|
+
import { normalizeHost } from '../machine-id.js';
|
|
32
|
+
import { checkCliAvailable, checkCliSignedIn } from './agents.js';
|
|
33
|
+
/** Per-remote readiness probe budget — matches the health probe's short window
|
|
34
|
+
* (`agents view` on a warm box is sub-second; a wedged one degrades to unknown). */
|
|
35
|
+
export const READY_PROBE_TIMEOUT_MS = 8_000;
|
|
36
|
+
/** How long a probed pool snapshot is reused within a `teams start` wave. Short
|
|
37
|
+
* enough that a device coming online / going overloaded is seen next wave, long
|
|
38
|
+
* enough that placing a wave of teammates does not re-fan-out per teammate. */
|
|
39
|
+
export const SIGNAL_TTL_MS = 15_000;
|
|
40
|
+
const cache = new Map();
|
|
41
|
+
function cacheKey(pool, agent) {
|
|
42
|
+
return `${agent}::${[...pool].map(normalizeHost).sort().join(',')}`;
|
|
43
|
+
}
|
|
44
|
+
/** Clear the probe cache — for tests and after a device-registry change. */
|
|
45
|
+
export function clearPlacementSignalCache() {
|
|
46
|
+
cache.clear();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether the requested agent is installed on a REMOTE device, via one SSH
|
|
50
|
+
* readiness probe. `undefined` when the probe could not answer (ssh/login
|
|
51
|
+
* failure) — reachability is then left to {@link probeFleetStats}; `false` when
|
|
52
|
+
* the box answered but agents-cli or the agent is absent (a genuine can't-run).
|
|
53
|
+
*/
|
|
54
|
+
function probeRemoteInstalled(device, agent) {
|
|
55
|
+
let args;
|
|
56
|
+
let env;
|
|
57
|
+
try {
|
|
58
|
+
const shim = writeAskpassShim();
|
|
59
|
+
const cmd = buildReadyProbeCommand(device.shell === 'powershell' ? 'windows' : undefined);
|
|
60
|
+
// agentOnly: a read-only probe must never force a foreground Touch ID sheet
|
|
61
|
+
// on a password-auth device (mirrors probeDeviceStats in devices/health).
|
|
62
|
+
({ args, env } = buildSshInvocation(device, [cmd], shim, {}, { agentOnly: true }));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return Promise.resolve(undefined);
|
|
66
|
+
}
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
execFile('ssh', args, { encoding: 'utf-8', env: { ...process.env, ...env }, timeout: READY_PROBE_TIMEOUT_MS }, (err, stdout) => {
|
|
69
|
+
if (err || !stdout)
|
|
70
|
+
return resolve(undefined);
|
|
71
|
+
const probe = parseReadyProbe(stdout);
|
|
72
|
+
if (!probe.reachable)
|
|
73
|
+
return resolve(undefined); // ssh/login issue → unknown
|
|
74
|
+
if (!probe.version)
|
|
75
|
+
return resolve(false); // agents-cli missing → cannot run
|
|
76
|
+
resolve(viewHasAgent(probe.view, agent));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Probe every device in the team pool and return a name→signal map for the pure
|
|
82
|
+
* placement pick. Devices with no data at all are omitted (the pick then neither
|
|
83
|
+
* excludes nor prefers them). Never throws — a probe failure degrades to a
|
|
84
|
+
* missing/partial signal.
|
|
85
|
+
*/
|
|
86
|
+
export async function probePoolSignals(pool, agent, opts = {}) {
|
|
87
|
+
const now = opts.now ?? Date.now();
|
|
88
|
+
const key = cacheKey(pool, agent);
|
|
89
|
+
const cached = cache.get(key);
|
|
90
|
+
if (!opts.force && cached && now - cached.at < SIGNAL_TTL_MS)
|
|
91
|
+
return cached.signals;
|
|
92
|
+
const reg = loadDevicesSync();
|
|
93
|
+
const self = normalizeHost(localMachineId());
|
|
94
|
+
const lookup = (name) => reg[name] ?? reg[normalizeHost(name)];
|
|
95
|
+
const profiles = [];
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (const name of pool) {
|
|
98
|
+
const d = lookup(name);
|
|
99
|
+
if (d && !seen.has(d.name)) {
|
|
100
|
+
profiles.push(d);
|
|
101
|
+
seen.add(d.name);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const selfProfile = profiles.find((d) => normalizeHost(d.name) === self);
|
|
105
|
+
const stats = await probeFleetStats(profiles, { selfName: selfProfile?.name });
|
|
106
|
+
const installed = new Map(await Promise.all(profiles.map(async (d) => {
|
|
107
|
+
const isSelf = normalizeHost(d.name) === self;
|
|
108
|
+
if (isSelf) {
|
|
109
|
+
const inst = checkCliAvailable(agent)[0];
|
|
110
|
+
const signedIn = inst ? await checkCliSignedIn(agent) : undefined;
|
|
111
|
+
return [d.name, { installed: inst, signedIn }];
|
|
112
|
+
}
|
|
113
|
+
// Remote sign-in stays unknown (see the module docblock); the install
|
|
114
|
+
// gate is what drives the fail-loud.
|
|
115
|
+
return [d.name, { installed: await probeRemoteInstalled(d, agent), signedIn: undefined }];
|
|
116
|
+
})));
|
|
117
|
+
const signals = new Map();
|
|
118
|
+
for (const name of pool) {
|
|
119
|
+
const d = lookup(name);
|
|
120
|
+
const s = d ? stats.get(d.name) : undefined;
|
|
121
|
+
const inst = d ? installed.get(d.name) : undefined;
|
|
122
|
+
if (!s && !inst)
|
|
123
|
+
continue; // fully unknown device — leave it out of the map
|
|
124
|
+
signals.set(name, {
|
|
125
|
+
reachable: s?.reachable,
|
|
126
|
+
headroom: s ? headroom(s) : undefined,
|
|
127
|
+
loadPercent: s?.loadPercent,
|
|
128
|
+
memPercent: s?.memPercent,
|
|
129
|
+
installed: inst?.installed,
|
|
130
|
+
signedIn: inst?.signedIn,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
cache.set(key, { at: now, signals });
|
|
134
|
+
return signals;
|
|
135
|
+
}
|