mindvest-atlas 0.7.0 → 0.8.1
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/package.json +1 -1
- package/src/api/client.js +7 -0
- package/src/commands/remote-control.js +57 -4
- package/src/config.js +1 -1
- package/src/remote-control.js +9 -2
package/package.json
CHANGED
package/src/api/client.js
CHANGED
|
@@ -41,6 +41,13 @@ async function refreshAndStore(creds) {
|
|
|
41
41
|
return updated;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** The bearer a job uses to call the Atlas API directly. Exported so the runner
|
|
45
|
+
* can put it in a child's environment; empty when not signed in, because a
|
|
46
|
+
* missing token is a script's problem to report, not the runner's to crash on. */
|
|
47
|
+
export async function jobToken() {
|
|
48
|
+
try { return await currentAccessToken(); } catch { return ''; }
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
async function currentAccessToken() {
|
|
45
52
|
const env = envToken();
|
|
46
53
|
if (env) return env;
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
// a schedule that quietly broke, which is the failure the record exists to make
|
|
11
11
|
// visible.
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
|
-
import { API } from '../config.js';
|
|
14
|
-
import { requestJson } from '../api/client.js';
|
|
13
|
+
import { API, baseUrl } from '../config.js';
|
|
14
|
+
import { requestJson, jobToken } from '../api/client.js';
|
|
15
15
|
import { runStream } from '../util/stream-loop.js';
|
|
16
16
|
import { color, eprintln } from '../util/ui.js';
|
|
17
17
|
import {
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
setPaused, writeHelpers,
|
|
21
21
|
} from '../remote-control.js';
|
|
22
22
|
import fs from 'node:fs';
|
|
23
|
+
import os from 'node:os';
|
|
23
24
|
import path from 'node:path';
|
|
24
25
|
|
|
25
26
|
const iso = (ms) => new Date(ms).toISOString().replace(/\.\d+Z$/, 'Z');
|
|
@@ -107,6 +108,50 @@ async function handleAsk(ask, root) {
|
|
|
107
108
|
return answer(ask.request_id, { ok: false, error: 'Unknown request.' });
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
/**
|
|
112
|
+
* How many projects may run at once.
|
|
113
|
+
*
|
|
114
|
+
* Scaled to the machine rather than fixed at a timid number: these are mostly
|
|
115
|
+
* IO-bound jobs waiting on an API, so a modern laptop can carry far more than
|
|
116
|
+
* four, and holding a job back on an idle machine helps nobody. The floor of 4
|
|
117
|
+
* covers a single-core VM; the ceiling of 16 leaves the computer usable for the
|
|
118
|
+
* person sitting at it, which is the actual constraint — this is their machine,
|
|
119
|
+
* not a build server.
|
|
120
|
+
*
|
|
121
|
+
* This bounds RUNS, not threads. A single run is free to use every core it can
|
|
122
|
+
* (see the build prompt): one run per project is about not racing two copies of
|
|
123
|
+
* the same script over one folder, never about how hard that script may work.
|
|
124
|
+
*/
|
|
125
|
+
const MAX_PARALLEL = Math.max(4, Math.min(16, os.cpus().length || 4));
|
|
126
|
+
let active = 0;
|
|
127
|
+
const waiting = [];
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Run a job WITHOUT blocking the stream.
|
|
131
|
+
*
|
|
132
|
+
* Awaiting it inline was a real bug: a ten-minute script held the SSE loop, so
|
|
133
|
+
* no other job started, the keepalive stopped, and the web's questions about
|
|
134
|
+
* this computer went unanswered until it finished. The loop's job is to listen;
|
|
135
|
+
* the work belongs beside it.
|
|
136
|
+
*
|
|
137
|
+
* One run per PROJECT is still enforced by the marker inside runJob, so this
|
|
138
|
+
* parallelism is across projects only.
|
|
139
|
+
*/
|
|
140
|
+
function spawnJob(job, root, timeoutMs) {
|
|
141
|
+
const start = () => {
|
|
142
|
+
active += 1;
|
|
143
|
+
void runJob(job, root, timeoutMs)
|
|
144
|
+
.catch((err) => eprintln(color.red(`✗ ${err.message}`)))
|
|
145
|
+
.finally(() => {
|
|
146
|
+
active -= 1;
|
|
147
|
+
const next = waiting.shift();
|
|
148
|
+
if (next) next();
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
if (active < MAX_PARALLEL) start();
|
|
152
|
+
else waiting.push(start);
|
|
153
|
+
}
|
|
154
|
+
|
|
110
155
|
async function runJob(job, root, timeoutMs) {
|
|
111
156
|
const name = job.name || job.automation_id || 'job';
|
|
112
157
|
const started = Date.now();
|
|
@@ -150,6 +195,12 @@ async function runJob(job, root, timeoutMs) {
|
|
|
150
195
|
// Secrets first (shared .env, then the job's own), so a job's own env can
|
|
151
196
|
// deliberately override a shared key.
|
|
152
197
|
...jobEnv(dir, root),
|
|
198
|
+
// The job's own credentials for the Atlas API, so a script calls it directly
|
|
199
|
+
// instead of shelling out to the CLI once per ticker. The token is the SAME
|
|
200
|
+
// one this runner is signed in with — the job acts as its owner, which is
|
|
201
|
+
// the only identity it could sensibly have.
|
|
202
|
+
ATLAS_BASE_URL: baseUrl(),
|
|
203
|
+
ATLAS_TOKEN: await jobToken(),
|
|
153
204
|
...(job.env && typeof job.env === 'object'
|
|
154
205
|
? Object.fromEntries(Object.entries(job.env).map(([k, v]) => [String(k), String(v)])) : {}),
|
|
155
206
|
ATLAS_JOB_PAYLOAD: JSON.stringify(job.payload || {}),
|
|
@@ -205,12 +256,14 @@ export async function remoteControlCommand(args) {
|
|
|
205
256
|
}
|
|
206
257
|
const jobs = Array.isArray(pending) ? pending : (pending?.jobs || []);
|
|
207
258
|
if (jobs.length) eprintln(color.dim(`● ${jobs.length} job(s) queued while away`));
|
|
208
|
-
for (const job of jobs)
|
|
259
|
+
for (const job of jobs) spawnJob(job, root, timeoutMs);
|
|
209
260
|
},
|
|
210
261
|
onEvent: async ({ event, payload }) => {
|
|
211
262
|
if (event === 'job' || event === 'message' || event === 'run') {
|
|
212
|
-
|
|
263
|
+
spawnJob(payload, root, timeoutMs);
|
|
213
264
|
} else if (event === 'ask') {
|
|
265
|
+
// Questions ARE awaited: they are fast, and answering them in order
|
|
266
|
+
// keeps a reply matched to the request that asked for it.
|
|
214
267
|
await handleAsk(payload, root);
|
|
215
268
|
}
|
|
216
269
|
},
|
package/src/config.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
|
|
5
|
-
export const VERSION = '0.
|
|
5
|
+
export const VERSION = '0.8.1';
|
|
6
6
|
export const DEFAULT_BASE_URL = 'https://atlasmcp.finmanagerai.com';
|
|
7
7
|
export const DEFAULT_SCOPE = 'atlas broker';
|
|
8
8
|
export const CLIENT_NAME = 'Atlas CLI';
|
package/src/remote-control.js
CHANGED
|
@@ -28,8 +28,15 @@ export const DEFAULT_ROOT = '~/AtlasStudio';
|
|
|
28
28
|
* so nothing has to be declared anywhere.
|
|
29
29
|
*/
|
|
30
30
|
export const ENTRIES = ['main.py', 'main.js', 'main.mjs'];
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Hard ceiling on one job, so a hung script cannot hold a slot forever.
|
|
33
|
+
*
|
|
34
|
+
* Half an hour, not ten minutes: real work — a chain walk, a wide scan, a model
|
|
35
|
+
* call in the middle — routinely runs past ten, and killing an honest job
|
|
36
|
+
* because the limit was set for a toy one is the worse failure. A script that is
|
|
37
|
+
* genuinely stuck is still stopped.
|
|
38
|
+
*/
|
|
39
|
+
export const DEFAULT_TIMEOUT_MS = 1_800_000;
|
|
33
40
|
|
|
34
41
|
export function workspaceRoot(override) {
|
|
35
42
|
const raw = override || process.env.ATLAS_LOCAL_DIR
|