myxo-lang 1.5.0

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.
Files changed (58) hide show
  1. package/CONCURRENCY.md +207 -0
  2. package/HOW_IT_WORKS.md +235 -0
  3. package/INDEPENDENCE.md +24 -0
  4. package/MYXO_PROMPT.md +139 -0
  5. package/README.md +494 -0
  6. package/ROADMAP.md +200 -0
  7. package/SPEC.md +181 -0
  8. package/VISION.md +249 -0
  9. package/builtins.js +361 -0
  10. package/command-fence.js +75 -0
  11. package/errors.js +45 -0
  12. package/examples/agent.myx +31 -0
  13. package/examples/fenced-agent.js +78 -0
  14. package/examples/fib.myx +11 -0
  15. package/examples/fibers.myx +57 -0
  16. package/examples/flow-routing.myx +12 -0
  17. package/examples/geo.myx +12 -0
  18. package/examples/hello.myx +2 -0
  19. package/examples/host.js +30 -0
  20. package/examples/james-myxo-demo.js +60 -0
  21. package/examples/living-mesh.myx +22 -0
  22. package/examples/match.myx +18 -0
  23. package/examples/mathlib.js +8 -0
  24. package/examples/mathlib.pl +11 -0
  25. package/examples/mathlib.py +23 -0
  26. package/examples/mcp-host.js +37 -0
  27. package/examples/nexus-mesh.myx +22 -0
  28. package/examples/nexus.myx +29 -0
  29. package/examples/ouroboros.myx +2 -0
  30. package/examples/outward-gate.myx +23 -0
  31. package/examples/physarum.myx +75 -0
  32. package/examples/polyglot-host.js +17 -0
  33. package/examples/polyglot.myx +11 -0
  34. package/examples/resilient.myx +28 -0
  35. package/examples/scheduler.myx +46 -0
  36. package/examples/the-law.myx +27 -0
  37. package/examples/use-geo.myx +9 -0
  38. package/format.js +206 -0
  39. package/interpreter.js +1092 -0
  40. package/lexer.js +173 -0
  41. package/mcp-bridge.js +77 -0
  42. package/mcp-framing.js +34 -0
  43. package/mcp-server.js +57 -0
  44. package/myxo-concurrent.js +110 -0
  45. package/myxo-live-worker.js +30 -0
  46. package/myxo-live.js +83 -0
  47. package/myxo-lsp.js +226 -0
  48. package/myxo-par-worker.js +39 -0
  49. package/myxo-plan.js +207 -0
  50. package/myxo-run.js +63 -0
  51. package/myxo.js +296 -0
  52. package/package.json +27 -0
  53. package/parser.js +662 -0
  54. package/polyglot-host.js +39 -0
  55. package/polyglot.js +191 -0
  56. package/receipt.js +71 -0
  57. package/std.myx +88 -0
  58. package/tools/memo-fuzz.js +224 -0
package/lexer.js ADDED
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+ // lexer.js — turns Myxo source text into a flat list of tokens.
3
+ // Each token carries its line and column so errors can point at the source.
4
+
5
+ const { MyxoError } = require('./errors');
6
+
7
+ // The words that mean something structural in Myxo. Everything else that looks
8
+ // like a name is an identifier (a pathway or an agent).
9
+ const KEYWORDS = new Set([
10
+ 'seed', 'decay', 'emit',
11
+ 'when', 'otherwise',
12
+ 'reinforce', 'times',
13
+ 'for', 'each', 'in',
14
+ 'agent', 'report',
15
+ 'attempt', 'rescue', 'fail',
16
+ 'weave', 'expose', 'as', 'needs',
17
+ 'live', 'dead', 'void',
18
+ 'and', 'or', 'not',
19
+ 'test', 'expect', 'match',
20
+ 'dispatch', 'gather',
21
+ 'spawn', 'give', 'take', 'yield',
22
+ ]);
23
+
24
+ // Two-character operators must be matched before their single-char prefixes.
25
+ const TWO_CHAR = {
26
+ '>=': 'GE', '<=': 'LE', '==': 'EQ', '!=': 'NE',
27
+ };
28
+ const ONE_CHAR = {
29
+ '+': 'PLUS', '-': 'MINUS', '*': 'STAR', '/': 'SLASH', '%': 'PERCENT',
30
+ '>': 'GT', '<': 'LT', '=': 'ASSIGN',
31
+ '(': 'LPAREN', ')': 'RPAREN',
32
+ '{': 'LBRACE', '}': 'RBRACE',
33
+ '[': 'LBRACKET', ']': 'RBRACKET',
34
+ ',': 'COMMA', ':': 'COLON',
35
+ '|': 'PIPE',
36
+ };
37
+
38
+ function isDigit(c) { return c >= '0' && c <= '9'; }
39
+ function isIdentStart(c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_'; }
40
+ function isIdentPart(c) { return isIdentStart(c) || isDigit(c); }
41
+
42
+ // tokenize(src[, comments]) — if a `comments` array is passed, each `# ...` comment is recorded there as
43
+ // { line, col, text, trailing } (trailing = code already appeared on that line). Comments are NOT tokens; this
44
+ // is so the formatter can re-attach them using the REAL string/interpolation-aware scanner (not a weaker copy).
45
+ function tokenize(src, comments) {
46
+ const tokens = [];
47
+ let i = 0;
48
+ let line = 1;
49
+ let col = 1;
50
+ let lastTokenLine = 0; // for trailing-comment detection
51
+
52
+ const peek = (o = 0) => src[i + o];
53
+ const advance = () => {
54
+ const c = src[i++];
55
+ if (c === '\n') { line++; col = 1; } else { col++; }
56
+ return c;
57
+ };
58
+ const push = (type, value, startLine, startCol) => {
59
+ tokens.push({ type, value, line: startLine, col: startCol });
60
+ lastTokenLine = startLine;
61
+ };
62
+
63
+ while (i < src.length) {
64
+ const c = peek();
65
+
66
+ // Whitespace (including newlines — Myxo is newline-insensitive).
67
+ if (c === ' ' || c === '\t' || c === '\r' || c === '\n') { advance(); continue; }
68
+
69
+ // Comments run from '#' to end of line. (Reached only OUTSIDE strings/interpolation — the string branch
70
+ // below consumes any '#' within a literal — so capture here is string-safe.)
71
+ if (c === '#') {
72
+ const cl = line, cc = col;
73
+ advance();
74
+ let text = '';
75
+ while (i < src.length && peek() !== '\n') text += advance();
76
+ if (comments) comments.push({ line: cl, col: cc, text: text.trim(), trailing: lastTokenLine === cl });
77
+ continue;
78
+ }
79
+
80
+ const startLine = line, startCol = col;
81
+
82
+ // Numbers: integer or decimal.
83
+ if (isDigit(c)) {
84
+ let num = '';
85
+ while (i < src.length && isDigit(peek())) num += advance();
86
+ if (peek() === '.' && isDigit(peek(1))) {
87
+ num += advance(); // the dot
88
+ while (i < src.length && isDigit(peek())) num += advance();
89
+ }
90
+ push('NUMBER', parseFloat(num), startLine, startCol);
91
+ continue;
92
+ }
93
+
94
+ // Strings: double-quoted, with escapes and {expr} interpolation.
95
+ if (c === '"') {
96
+ advance(); // opening quote
97
+ const segs = [];
98
+ let lit = '';
99
+ let interpolated = false;
100
+ while (i < src.length && peek() !== '"') {
101
+ const ch = peek();
102
+ if (ch === '\\') {
103
+ advance();
104
+ const esc = advance();
105
+ lit += esc === 'n' ? '\n' : esc === 't' ? '\t' : esc === '"' ? '"'
106
+ : esc === '\\' ? '\\' : esc === '{' ? '{' : esc === '}' ? '}' : esc;
107
+ continue;
108
+ }
109
+ if (ch === '{') {
110
+ interpolated = true;
111
+ if (lit) { segs.push({ t: 'lit', v: lit }); lit = ''; }
112
+ advance(); // consume the opening {
113
+ let depth = 1, exprSrc = '';
114
+ while (i < src.length && depth > 0) {
115
+ const e = peek();
116
+ if (e === '"') { // copy a nested string verbatim
117
+ exprSrc += advance();
118
+ while (i < src.length && peek() !== '"') {
119
+ if (peek() === '\\') exprSrc += advance();
120
+ exprSrc += advance();
121
+ }
122
+ if (i < src.length) exprSrc += advance();
123
+ continue;
124
+ }
125
+ if (e === '{') { depth++; exprSrc += advance(); continue; }
126
+ if (e === '}') { depth--; advance(); if (depth === 0) break; exprSrc += '}'; continue; }
127
+ exprSrc += advance();
128
+ }
129
+ if (depth > 0) throw new MyxoError('unterminated { } interpolation in string', startLine, startCol);
130
+ segs.push({ t: 'expr', v: exprSrc });
131
+ continue;
132
+ }
133
+ lit += advance();
134
+ }
135
+ if (i >= src.length) throw new MyxoError('unterminated string', startLine, startCol);
136
+ advance(); // closing quote
137
+ if (interpolated) {
138
+ if (lit) segs.push({ t: 'lit', v: lit });
139
+ push('TEMPLATE', segs, startLine, startCol);
140
+ } else {
141
+ push('STRING', lit, startLine, startCol);
142
+ }
143
+ continue;
144
+ }
145
+
146
+ // Identifiers and keywords.
147
+ if (isIdentStart(c)) {
148
+ let name = '';
149
+ while (i < src.length && isIdentPart(peek())) name += advance();
150
+ push(KEYWORDS.has(name) ? 'KEYWORD' : 'IDENT', name, startLine, startCol);
151
+ continue;
152
+ }
153
+
154
+ // Three-dot ellipsis: a rest parameter (`...rest`).
155
+ if (c === '.' && peek(1) === '.' && peek(2) === '.') {
156
+ advance(); advance(); advance();
157
+ push('ELLIPSIS', '...', startLine, startCol);
158
+ continue;
159
+ }
160
+
161
+ // Operators and punctuation.
162
+ const two = c + (peek(1) || '');
163
+ if (TWO_CHAR[two]) { advance(); advance(); push(TWO_CHAR[two], two, startLine, startCol); continue; }
164
+ if (ONE_CHAR[c]) { advance(); push(ONE_CHAR[c], c, startLine, startCol); continue; }
165
+
166
+ throw new MyxoError(`unexpected character '${c}'`, startLine, startCol);
167
+ }
168
+
169
+ push('EOF', null, line, col);
170
+ return tokens;
171
+ }
172
+
173
+ module.exports = { tokenize, KEYWORDS };
package/mcp-bridge.js ADDED
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+ // mcp-bridge.js — the seam that makes Myxo the one fenced surface every tool speaks
3
+ // through. It turns a catalog of MCP tools into Myxo capabilities: each tool becomes
4
+ // a native, and Myxo's manifest (`needs`), value budgets, and audit ledger then
5
+ // govern every call. Whatever language or service implements a tool — Python, Rust,
6
+ // a shell, an HTTP endpoint, a model — from inside an Myxo script it's just a verb the
7
+ // script was granted. That is the mesh: many languages, one law.
8
+ //
9
+ // The bridge is transport-agnostic on purpose. You hand it a `client`:
10
+ // { tools: [{ name, description, inputSchema }], call(name, argsObject) -> result }
11
+ // `call` is SYNCHRONOUS (returns the result, not a Promise). A Node host wrapping a
12
+ // live, async MCP server supplies its own sync-invoking shim; Myxo stays pure and the
13
+ // fence stays the host's only grant surface. Async-native execution is the next stone.
14
+
15
+ const { VOID } = require('./interpreter');
16
+ const { MyxoError } = require('./errors');
17
+
18
+ // ---- value mapping: Myxo values <-> plain JS (the wire between worlds) ----------
19
+
20
+ function nxToJs(v) {
21
+ if (v === VOID || v === undefined) return null;
22
+ if (Array.isArray(v)) return v.map(nxToJs);
23
+ if (v instanceof Map) { const o = {}; for (const [k, val] of v) o[k] = nxToJs(val); return o; }
24
+ return v; // number, string, bool pass straight through
25
+ }
26
+
27
+ function jsToNx(v) {
28
+ if (v === null || v === undefined) return VOID;
29
+ if (Array.isArray(v)) return v.map(jsToNx);
30
+ if (typeof v === 'object') { const m = new Map(); for (const k of Object.keys(v)) m.set(k, jsToNx(v[k])); return m; }
31
+ return v;
32
+ }
33
+
34
+ // Most MCP tools answer with a content envelope: { content: [{type:'text', text}], isError }.
35
+ // Unwrap that to the plain text a script wants; surface an error result as an MyxoError so
36
+ // it lands in the audit ledger and an enclosing `attempt` can rescue it.
37
+ function unwrapResult(res) {
38
+ if (res && typeof res === 'object' && Array.isArray(res.content)) {
39
+ const text = res.content.filter(c => c && c.type === 'text').map(c => c.text).join('\n');
40
+ if (res.isError) throw new MyxoError(text || 'MCP tool reported an error');
41
+ return text;
42
+ }
43
+ return res;
44
+ }
45
+
46
+ // Turn one Myxo call's args into the named-arguments object an MCP tool expects.
47
+ // A mesh maps straight to the object; for ergonomics a lone primitive fills the
48
+ // tool's first required (or first declared) property, so `query("SELECT ...")` works.
49
+ function buildArgs(tool, args) {
50
+ if (args.length === 0) return {};
51
+ const a = args[0];
52
+ if (a instanceof Map) return nxToJs(a);
53
+ const props = tool.inputSchema && tool.inputSchema.properties;
54
+ if (props) {
55
+ const required = (tool.inputSchema.required && tool.inputSchema.required[0]) || Object.keys(props)[0];
56
+ if (required) return { [required]: nxToJs(a) };
57
+ }
58
+ throw new MyxoError(`MCP tool '${tool.name}' needs a mesh of named arguments, e.g. ${tool.name}({ ... })`);
59
+ }
60
+
61
+ // Register every tool in `client` as a fenced Myxo capability on `interp`.
62
+ // Returns the list of bridged tool names. After this, an Myxo script reaches each
63
+ // tool by name — bounded by its own `needs` manifest and logged in the audit ledger.
64
+ function bridgeMcpTools(interp, client) {
65
+ if (!client || !Array.isArray(client.tools) || typeof client.call !== 'function') {
66
+ throw new Error('bridgeMcpTools needs a client: { tools: [...], call(name, args) }');
67
+ }
68
+ for (const tool of client.tools) {
69
+ interp.registerNative(tool.name, (args) => {
70
+ const argObj = buildArgs(tool, args);
71
+ return jsToNx(unwrapResult(client.call(tool.name, argObj)));
72
+ }, true); // capability = true -> fenced by `needs`, recorded in the audit ledger
73
+ }
74
+ return client.tools.map(t => t.name);
75
+ }
76
+
77
+ module.exports = { bridgeMcpTools, nxToJs, jsToNx, unwrapResult };
package/mcp-framing.js ADDED
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+ // mcp-framing.js — newline-delimited JSON message framing for the MCP wire.
3
+ //
4
+ // The provider-side companion to mcp-bridge.js (which bridges tools INTO Myxo as
5
+ // fenced capabilities). This is the transport plumbing for Myxo to SERVE tools over
6
+ // MCP: one JSON-RPC value per line. JSON.stringify never emits a raw newline, so
7
+ // '\n' is a safe delimiter both ways (server<->client) over stdio.
8
+
9
+ function serialize(msg) {
10
+ return JSON.stringify(msg) + '\n';
11
+ }
12
+
13
+ // Accumulates incoming text and yields complete JSON messages as they arrive,
14
+ // holding any trailing partial line until the rest shows up.
15
+ class MessageBuffer {
16
+ constructor() {
17
+ this._buf = '';
18
+ }
19
+
20
+ push(chunk) {
21
+ this._buf += chunk;
22
+ const out = [];
23
+ let idx;
24
+ while ((idx = this._buf.indexOf('\n')) !== -1) {
25
+ const line = this._buf.slice(0, idx);
26
+ this._buf = this._buf.slice(idx + 1);
27
+ if (line.trim() === '') continue;
28
+ out.push(JSON.parse(line));
29
+ }
30
+ return out;
31
+ }
32
+ }
33
+
34
+ module.exports = { MessageBuffer, serialize };
package/mcp-server.js ADDED
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+ // mcp-server.js — the provider side of MCP for Myxo: serve a tool catalog over
3
+ // JSON-RPC 2.0. The mirror of mcp-bridge.js (which bridges tools INTO Myxo). Ours,
4
+ // zero-dependency, drop-in for the low-level @modelcontextprotocol/sdk surface
5
+ // (Server + setRequestHandler + the request-schema tags). The stdio transport and
6
+ // connect() arrive in the next stone, wired in via handleMessage().
7
+
8
+ // Request "schemas" are just method tags — setRequestHandler keys off .method,
9
+ // so a consumer's `setRequestHandler(ListToolsRequestSchema, fn)` registers fn for
10
+ // the 'tools/list' method exactly as the SDK does.
11
+ const ListToolsRequestSchema = { method: 'tools/list' };
12
+ const CallToolRequestSchema = { method: 'tools/call' };
13
+
14
+ const DEFAULT_PROTOCOL = '2025-06-18';
15
+
16
+ class Server {
17
+ constructor(info, opts = {}) {
18
+ this._info = { name: info.name, version: info.version };
19
+ this._capabilities = (opts && opts.capabilities) || {};
20
+ this._handlers = new Map(); // method -> async handler(request, extra)
21
+ }
22
+
23
+ setRequestHandler(schema, handler) {
24
+ this._handlers.set(schema.method, handler);
25
+ }
26
+
27
+ // Turn one incoming JSON-RPC message into its response object, or null for a
28
+ // notification (no id -> nothing is sent back). Never throws: a handler failure
29
+ // becomes a JSON-RPC error response so the wire keeps moving.
30
+ async handleMessage(msg) {
31
+ if (msg == null || msg.id === undefined || msg.id === null) return null; // notification
32
+ const id = msg.id;
33
+ try {
34
+ if (msg.method === 'initialize') {
35
+ return {
36
+ jsonrpc: '2.0',
37
+ id,
38
+ result: {
39
+ protocolVersion: (msg.params && msg.params.protocolVersion) || DEFAULT_PROTOCOL,
40
+ capabilities: this._capabilities,
41
+ serverInfo: this._info,
42
+ },
43
+ };
44
+ }
45
+ const handler = this._handlers.get(msg.method);
46
+ if (!handler) {
47
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${msg.method}` } };
48
+ }
49
+ const result = await handler(msg, {});
50
+ return { jsonrpc: '2.0', id, result };
51
+ } catch (e) {
52
+ return { jsonrpc: '2.0', id, error: { code: -32603, message: (e && e.message) || String(e) } };
53
+ }
54
+ }
55
+ }
56
+
57
+ module.exports = { Server, ListToolsRequestSchema, CallToolRequestSchema, DEFAULT_PROTOCOL };
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+ // myxo-concurrent.js — the parent half of Myxo parallelism. Spawns one worker per task, runs them on real OS
3
+ // threads, and blocks the calling thread on an Atomics barrier until all finish — so the synchronous `gather`
4
+ // gets genuine multi-core parallelism with no callbacks. Built on the same worker+Atomics pattern as myxo-live.
5
+ //
6
+ // runParallel(tasks, opts) -> [results] (plain JS values), in input order.
7
+ // tasks[i] = { name, params, body, args } args are plain JS (already myxo->js converted)
8
+ // throws on the first task error, or on a wall-clock timeout (workers are always terminated).
9
+
10
+ const { Worker, MessageChannel, receiveMessageOnPort } = require('worker_threads');
11
+ const path = require('path');
12
+
13
+ // Native delivery can lag a hair behind the Atomics tick, so the drain spins briefly. This is a backstop,
14
+ // not the mechanism: a worker posts its message BEFORE ticking the barrier (see myxo-par-worker.js), so once
15
+ // the barrier is satisfied the message is already queued. The spin only covers nanosecond delivery latency.
16
+ const DRAIN_SPINS = 5000000;
17
+
18
+ // A value that crosses the thread boundary must be plain DATA. Code (agents/natives) closes over an
19
+ // environment that cannot follow it into an isolated worker, so we reject it loudly at the boundary
20
+ // instead of silently shipping a meaningless AST blob. VOID arrives as a symbol — that is fine (-> null).
21
+ function assertSerializable(v, label, seen) {
22
+ if (v === null || v === undefined) return;
23
+ const t = typeof v;
24
+ if (t === 'number') {
25
+ // JSON renders NaN/Infinity as "null", so they would arrive as void — a silent wrong answer. Reject
26
+ // them at the boundary, exactly as polyglot.js does for the language bridge: never a silent null.
27
+ if (!Number.isFinite(v)) throw new Error(`${label} cannot be a non-finite number (NaN or Infinity)`);
28
+ return;
29
+ }
30
+ if (t === 'string' || t === 'boolean' || t === 'symbol') return; // symbol = VOID, fine (-> null)
31
+ if (t === 'function') throw new Error(`${label} cannot be a function`);
32
+ if (v.__agent || v.__native || v.__task) {
33
+ throw new Error(`${label} must be data (number, string, bool, list, mesh) — not an agent or task`);
34
+ }
35
+ seen = seen || new Set();
36
+ if (seen.has(v)) throw new Error(`${label} cannot be cyclic`);
37
+ seen.add(v);
38
+ if (Array.isArray(v)) { for (const x of v) assertSerializable(x, label, seen); seen.delete(v); return; }
39
+ if (v instanceof Map) { // validate keys too (future-proof; today keys are strings)
40
+ for (const [k, x] of v) { assertSerializable(k, label, seen); assertSerializable(x, label, seen); }
41
+ seen.delete(v); return;
42
+ }
43
+ throw new Error(`${label} must be data (number, string, bool, list, mesh)`);
44
+ }
45
+
46
+ // The settled core: run every task on its own worker thread, block on the Atomics barrier, and return ONE
47
+ // outcome per task IN ORDER — `{ ok:true, value, ms }` or `{ ok:false, error }`. It never throws on a task
48
+ // failure (the caller decides policy: gather fails fast, the scheduler reroutes); it throws only on the
49
+ // wall-clock timeout. `ms` is the worker's self-measured compute time — the scheduler's conductance signal.
50
+ function runSettled(tasks, opts = {}) {
51
+ const n = tasks.length;
52
+ if (n === 0) return [];
53
+ const timeoutMs = Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : 30000;
54
+ const now = opts.now || (() => Date.now());
55
+
56
+ const counter = new Int32Array(new SharedArrayBuffer(4)); // workers tick this; we wait on it
57
+ const workers = [];
58
+ const ports = [];
59
+ const cleanup = () => { for (const w of workers) w.terminate(); for (const p of ports) p.close(); };
60
+
61
+ try {
62
+ for (let i = 0; i < n; i++) {
63
+ const { port1, port2 } = new MessageChannel();
64
+ const t = tasks[i];
65
+ const w = new Worker(path.join(__dirname, 'myxo-par-worker.js'), {
66
+ workerData: { name: t.name, params: t.params, body: t.body, argsJSON: JSON.stringify(t.args), counterSab: counter.buffer, port: port2 },
67
+ transferList: [port2],
68
+ });
69
+ // These listeners exist ONLY so a worker failure can't crash the parent PROCESS — they cannot release
70
+ // the barrier, because the event loop is frozen while we sit in Atomics.wait below. A worker that runs
71
+ // any JS at all ticks the barrier itself from a `finally` (myxo-par-worker.js); a true hard death (OOM,
72
+ // SIGKILL) that never runs that `finally` is caught by the wall-clock timeout. No callback is relied on.
73
+ w.on('error', () => {});
74
+ w.on('exit', () => {});
75
+ workers.push(w);
76
+ ports.push(port1);
77
+ }
78
+
79
+ const deadline = now() + timeoutMs; // barrier: block until every worker has ticked
80
+ while (Atomics.load(counter, 0) < n) {
81
+ const c = Atomics.load(counter, 0); // re-read each pass so an increment racing the wait can't be lost
82
+ const remaining = deadline - now();
83
+ if (remaining <= 0) throw new Error(`gather timed out after ${timeoutMs}ms (a worker never responded — possible hard crash or OOM)`);
84
+ Atomics.wait(counter, 0, c, Math.min(remaining, 1000));
85
+ }
86
+
87
+ const outcomes = new Array(n);
88
+ for (let i = 0; i < n; i++) {
89
+ let m = receiveMessageOnPort(ports[i]);
90
+ let spins = 0;
91
+ while (!m && spins < DRAIN_SPINS) { m = receiveMessageOnPort(ports[i]); spins++; }
92
+ if (!m) { outcomes[i] = { ok: false, error: `no result from worker ${i}` }; continue; }
93
+ const r = m.message;
94
+ outcomes[i] = r.ok ? { ok: true, value: JSON.parse(r.resultJSON), ms: r.ms } : { ok: false, error: r.error };
95
+ }
96
+ return outcomes;
97
+ } finally {
98
+ cleanup(); // always reclaim threads + ports — even on throw/timeout
99
+ }
100
+ }
101
+
102
+ // gather's runner: first error wins (all-or-nothing, like Promise.all); returns plain values in order.
103
+ function runParallel(tasks, opts = {}) {
104
+ const outcomes = runSettled(tasks, opts);
105
+ const bad = outcomes.find(o => o && !o.ok);
106
+ if (bad) throw new Error(bad.error);
107
+ return outcomes.map(o => o.value);
108
+ }
109
+
110
+ module.exports = { runParallel, runSettled, assertSerializable };
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+ // myxo-live-worker.js — the Worker half of myxo-live. It runs the (synchronous) Myxo script,
3
+ // and whenever the script invokes a tool it posts the request to the parent and parks on
4
+ // Atomics.wait until the parent posts the result back. This is what lets a synchronous
5
+ // interpreter call asynchronous tools without the interpreter ever knowing.
6
+
7
+ const { workerData, parentPort, receiveMessageOnPort } = require('worker_threads');
8
+ const { runScript } = require('./myxo-run');
9
+
10
+ const { script, tools, allow, dir, maxDepth, maxSteps, requireManifest, valueCaps, moduleLoader, sab, port } = workerData;
11
+ const signal = new Int32Array(sab);
12
+
13
+ const client = {
14
+ tools,
15
+ // synchronous from Myxo's view: post -> block -> read the answer the parent left us
16
+ call(name, args) {
17
+ Atomics.store(signal, 0, 0);
18
+ port.postMessage({ type: 'call', name, args });
19
+ Atomics.wait(signal, 0, 0); // parked until the parent stores 1 + notifies
20
+ let m = receiveMessageOnPort(port);
21
+ let spins = 0;
22
+ while (!m && spins < 5000000) { m = receiveMessageOnPort(port); spins++; } // guard delivery lag
23
+ if (!m) throw new Error('no response from host for tool ' + name);
24
+ if (m.message.error) throw new Error(m.message.error);
25
+ return m.message.result;
26
+ },
27
+ };
28
+
29
+ const result = runScript(script, { client, allow, dir, maxDepth, maxSteps, requireManifest, valueCaps, moduleLoader });
30
+ parentPort.postMessage({ type: 'done', result });
package/myxo-live.js ADDED
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+ // myxo-live.js — run an Myxo script against ASYNC tools, synchronously.
3
+ //
4
+ // Myxo's interpreter is synchronous (a tree-walker), but real-world tools — MCP calls,
5
+ // HTTP, a database — are async. This bridges that gap with the zero-dependency
6
+ // worker+Atomics pattern: the script runs inside a Worker (sync), and every tool call
7
+ // round-trips to the parent's async handler. The Worker blocks on `Atomics.wait` until
8
+ // the parent posts the result back, so from inside Myxo the call looks like a plain value.
9
+ //
10
+ // The transport is abstract: you supply `onCall(name, argsObject) -> Promise<result>`.
11
+ // Whether that's an in-process MCP dispatch, an HTTP fetch, or a db query, myxo-live does
12
+ // not care — which is what lets the same bridge wire into any host (e.g. james_nx_run).
13
+
14
+ const { Worker, MessageChannel } = require('worker_threads');
15
+ const path = require('path');
16
+
17
+ // runLive(script, opts) -> Promise<{ ok, output, audit, error }>
18
+ // opts.tools: [{ name, inputSchema }] the catalog to bridge as fenced capabilities
19
+ // opts.onCall: async (name, argsObject) => result the real async invoker
20
+ // opts.allow: optional host allowlist of tool names (defense in depth)
21
+ // opts.dir, opts.maxDepth, opts.maxSteps, opts.requireManifest: passed through to the runner
22
+ // opts.timeoutMs: wall-clock kill switch for the worker (default 30000)
23
+ function runLive(script, opts = {}) {
24
+ const { tools = [], onCall, allow, dir, maxDepth, valueCaps } = opts;
25
+ const requireManifest = opts.requireManifest !== undefined ? !!opts.requireManifest : true;
26
+ const maxSteps = opts.maxSteps !== undefined ? opts.maxSteps : 200000;
27
+ const moduleLoader = opts.moduleLoader !== undefined ? opts.moduleLoader : null;
28
+ const timeoutMs = Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : 30000;
29
+ if (typeof onCall !== 'function') {
30
+ return Promise.reject(new Error('runLive needs an async onCall(name, args)'));
31
+ }
32
+ if (typeof moduleLoader === 'function') {
33
+ return Promise.reject(new Error('runLive cannot transfer a function moduleLoader into the worker'));
34
+ }
35
+ return new Promise((resolve, reject) => {
36
+ const sab = new SharedArrayBuffer(4);
37
+ const signal = new Int32Array(sab);
38
+ const { port1, port2 } = new MessageChannel();
39
+ let settled = false;
40
+ let timer = null;
41
+
42
+ const worker = new Worker(path.join(__dirname, 'myxo-live-worker.js'), {
43
+ workerData: { script, tools, allow, dir, maxDepth, maxSteps, requireManifest, valueCaps, moduleLoader, sab, port: port2 },
44
+ transferList: [port2],
45
+ });
46
+
47
+ const finish = (fn, v) => {
48
+ if (settled) return;
49
+ settled = true;
50
+ if (timer) clearTimeout(timer);
51
+ port1.close();
52
+ worker.terminate();
53
+ fn(v);
54
+ };
55
+ timer = setTimeout(() => {
56
+ finish(resolve, { ok: false, output: '', audit: [], error: `Myxo run timed out after ${timeoutMs}ms` });
57
+ }, timeoutMs);
58
+
59
+ // Each privileged call surfaces here as a 'call' message. Do the async work, post the
60
+ // result back to the Worker, then wake it. The Worker is parked in Atomics.wait.
61
+ port1.on('message', (msg) => {
62
+ if (!msg || msg.type !== 'call') return;
63
+ Promise.resolve()
64
+ .then(() => onCall(msg.name, msg.args))
65
+ .then(
66
+ (result) => { if (!settled) port1.postMessage({ result, error: null }); },
67
+ (e) => { if (!settled) port1.postMessage({ result: null, error: e && e.message ? e.message : String(e) }); },
68
+ )
69
+ .then(() => {
70
+ if (!settled) {
71
+ Atomics.store(signal, 0, 1);
72
+ Atomics.notify(signal, 0);
73
+ }
74
+ });
75
+ });
76
+
77
+ worker.on('message', (m) => { if (m && m.type === 'done') finish(resolve, m.result); });
78
+ worker.on('error', (e) => finish(reject, e));
79
+ worker.on('exit', (code) => { if (!settled && code !== 0) finish(reject, new Error('myxo worker exited ' + code)); });
80
+ });
81
+ }
82
+
83
+ module.exports = { runLive };