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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindvest-atlas",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Atlas CLI \u2014 OAuth login, tool calls, and live alert/flow streaming for the Atlas trading API",
5
5
  "type": "module",
6
6
  "bin": {
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) await runJob(job, root, timeoutMs);
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
- await runJob(payload, root, timeoutMs);
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.7.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';
@@ -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
- /** Hard ceiling on one job, so a hung script cannot hold the runner forever. */
32
- export const DEFAULT_TIMEOUT_MS = 600_000;
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