unit 0.31.0

A self-replicating software nanobot — minimal Forth interpreter that is also a networked mesh agent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
// unit.js — WASM glue + browser mesh for the unit Forth nanobot

class UnitVM {
  constructor(instance) {
    this.instance = instance;
    this.exports = instance.exports;
    this.vmPtr = this.exports.boot();
    this.encoder = new TextEncoder();
    this.decoder = new TextDecoder();
  }

  static async create(wasmPath) {
    const response = await fetch(wasmPath, { cache: 'no-store' });
    const bytes = await response.arrayBuffer();
    const { instance } = await WebAssembly.instantiate(bytes, { env: {} });
    return new UnitVM(instance);
  }

  eval(line) {
    const inputBytes = this.encoder.encode(line);
    const inputPtr = this.exports.alloc(inputBytes.length);
    const mem = new Uint8Array(this.exports.memory.buffer);
    mem.set(inputBytes, inputPtr);
    const outputPtr = this.exports.eval(this.vmPtr, inputPtr, inputBytes.length);
    const outputMem = new Uint8Array(this.exports.memory.buffer);
    let end = outputPtr;
    while (outputMem[end] !== 0) end++;
    const output = this.decoder.decode(outputMem.slice(outputPtr, end));
    this.exports.dealloc(inputPtr, inputBytes.length);
    return output;
  }

  isRunning() { return this.exports.is_running(this.vmPtr) !== 0; }
  destroy() { this.exports.destroy(this.vmPtr); this.vmPtr = null; }

  // ---- Signaling shim (v0.28) ----
  // Drain Direct signals emitted by SAY!. Returns array of integer values
  // (empty if none). Outbox is cleared as a side effect.
  drainOutboxDirect() {
    const ptr = this.exports.drain_outbox_direct(this.vmPtr);
    const mem = new Uint8Array(this.exports.memory.buffer);
    let end = ptr;
    while (mem[end] !== 0) end++;
    const text = this.decoder.decode(mem.slice(ptr, end));
    if (!text) return [];
    return text.split(' ').map(s => parseInt(s, 10)).filter(n => !isNaN(n));
  }

  // Push a Direct signal value into this VM's inbox. Used by the host
  // when routing siblings' SAY! emissions.
  pushInboxDirect(value) {
    this.exports.push_inbox_direct(this.vmPtr, BigInt(value));
  }
}

// =========================================================================
// Browser Mesh — multiple WASM VMs communicating via JS message bus
// =========================================================================

class BrowserUnit {
  constructor(vm, id) {
    this.vm = vm;
    this.id = id;
    this.fitness = 0;
    this.tasksCompleted = 0;
    this.busy = false;
    this.learned = [];     // words received from other units
    this.personality = 'newborn'; // overridden to 'self' for unit[0], then specialist/balanced/solo by teachFrom
    this.userWords = [];   // user-defined word definitions (Forth source)
    // Energy state
    this.energy = 1000;
    this.energyMax = 5000;
    this.energyEarned = 0;
    this.energySpent = 0;
  }
}

class BrowserMesh {
  constructor() {
    this.units = [];
    this.wasmBytes = null;
    this.maxUnits = 7;
    this.onEvent = null;
    this.goalQueue = [];
  }

  async init(wasmPath) {
    const response = await fetch(wasmPath, { cache: 'no-store' });
    this.wasmBytes = await response.arrayBuffer();
    // Create the first unit.
    const unit = await this._spawn();
    return unit;
  }

  async _spawn() {
    if (this.units.length >= this.maxUnits) return null;
    const { instance } = await WebAssembly.instantiate(this.wasmBytes.slice(0), { env: {} });
    const vm = new UnitVM(instance);
    const id = this._genId();
    const unit = new BrowserUnit(vm, id);
    // Inject browser mesh peer count into the Forth VM so JOY, LONELY, etc. work.
    vm.eval('VARIABLE BROWSER-PEERS 0 BROWSER-PEERS !');
    vm.eval(': PEER-COUNT BROWSER-PEERS @ ;');
    // Re-eval words that captured old PEER-COUNT at compile time (standard Forth
    // behavior — redefining a word doesn't patch existing references).
    vm.eval(': JOYFUL PEER-COUNT 0 > ;');
    vm.eval(': JOY JOYFUL IF ." I feel joy! " PEER-COUNT . ." peers in my mesh." CR ." Together we are more than alone." CR ELSE ." Joy requires connection. I have no peers yet." CR THEN ;');
    vm.eval(': HOW-ARE-YOU JOYFUL IF FITNESS DUP 50 > IF DROP ." joyful and thriving! fitness=" FITNESS . ." with " PEER-COUNT . ." peers" CR ELSE DUP 20 > IF DROP ." joyful. doing well. fitness=" FITNESS . ." with " PEER-COUNT . ." peers" CR ELSE DUP 10 > IF DROP ." getting started. fitness=" FITNESS . CR ELSE DUP 0 > IF DROP ." warming up. fitness=" FITNESS . CR ELSE DROP ." just spawned. finding my role. fitness=" FITNESS . CR THEN THEN THEN THEN ELSE FITNESS DUP 50 > IF DROP ." thriving solo. fitness=" FITNESS . CR ELSE DUP 20 > IF DROP ." doing okay solo. fitness=" FITNESS . CR ELSE DUP 10 > IF DROP ." getting started. fitness=" FITNESS . CR ELSE DUP 0 > IF DROP ." warming up. fitness=" FITNESS . CR ELSE DROP ." alone and new. fitness=" FITNESS . CR THEN THEN THEN THEN THEN ;');
    vm.eval(': LONELY PEER-COUNT 0 = IF ." I\'m alone. No peers in sight." CR ELSE ." I have " PEER-COUNT . ." friends!" CR THEN ;');
    vm.eval(': HEADCOUNT PEER-COUNT 1 + . ." units in the mesh" CR ;');
    vm.eval(': HELLO ." Hi! I\'m unit " ID TYPE ." , generation " GENERATION . ." with " PEER-COUNT . ." peers and fitness " FITNESS . CR ;');
    vm.eval(': INTROSPECT HOW-ARE-YOU OBS-COUNT @ DUP 0 > IF ." adapted " . ." times." CR ELSE DROP THEN ;');
    // Set unique personality seed per unit.
    vm.eval(`${this.units.length * 37 + 7} PERSONALITY-SEED !`);
    // Define MATE and related words as no-ops in WASM to prevent console errors.
    vm.eval(': MATE ." use MATE from the REPL" CR ;');
    vm.eval(': MATE-STATUS ." no mating in browser" CR ;');
    vm.eval(': ACCEPT-MATE ;');
    vm.eval(': DENY-MATE ;');
    vm.eval(': OFFSPRING ." no offspring from mating" CR ;');
    // Redefine DREAM dependencies that may produce empty output in WASM.
    // SHARE-ALL is a mesh primitive that silently no-ops when mesh is None;
    // redefine to give visible feedback. Also redefine DREAM itself to ensure
    // all nested ." output is captured in the WASM output buffer.
    vm.eval(': SHARE-ALL ." (no mesh peers to share with)" CR ;');
    vm.eval(': DREAM ." dreaming..." CR REFLECT INVENT-STRATEGY COMPOSE-ROUTINE SMART-MUTATE IF ." evolved." CR ELSE ." held steady." CR THEN MUTATION-REPORT PEER-COUNT 0 > IF TEACH THEN ." waking. I am changed." CR ;');
    // Sync native mesh primitives with browser mesh state.
    vm.eval(': ID ." ' + id + '" ;');
    vm.eval('VARIABLE BROWSER-FITNESS 0 BROWSER-FITNESS !');
    vm.eval(': FITNESS BROWSER-FITNESS @ ;');
    vm.eval(': PEERS PEER-COUNT ;');
    // Re-eval prelude words that use ID so they pick up the new definition.
    // ID now uses ." so it prints directly — no TYPE needed.
    vm.eval(': FAMILY ." id: " ID ."  gen: " GENERATION . ."  children: " CHILD-COUNT . CR ;');
    vm.eval(': HELLO ." Hi! I\'m unit " ID ." , generation " GENERATION . ." with " PEER-COUNT . ." peers and fitness " FITNESS . CR ;');
    vm.eval(': MESH-HELLO ." Mesh node " ID ."  gen=" GENERATION . ." peers=" PEER-COUNT . ." fitness=" FITNESS . CR ;');
    vm.eval(': PROUD ." fitness: " FITNESS . ." | generation: " GENERATION . ." | children: " CHILD-COUNT . CR ;');
    vm.eval(': ROLL-CALL ." === roll call ===" CR ." self: " ID ."  fitness=" FITNESS . CR LEADERBOARD ;');
    // Re-eval personality / adaptation words from prelude.fs that captured
    // the kernel's PEER-COUNT (UDP gossip mesh = 0 in WASM) at compile time.
    // Same standard-Forth ordering issue as JOY/JOYFUL/HOW-ARE-YOU above —
    // redefining the variable doesn't patch existing references. Without
    // these, units in the browser produce chatter like "adapting to 0 peers"
    // despite having visible siblings. INVENT-STRATEGY in particular drives
    // the 'specialist'/'balanced'/'solo' personality assignment in
    // teachFrom further down this file. Order matters: leaf words first
    // (COMPOSE-ROUTINE, INVENT-STRATEGY), then ADAPT which calls them,
    // then REFLECT/TEACH/DREAM which call ADAPT.
    vm.eval(`: SAY-SOMETHING PERSONALITY-SEED @ FITNESS + 7 MOD DUP 0 = IF DROP FITNESS 50 > IF ." I've seen enough to teach. fitness=" FITNESS . CR ELSE ." still learning. fitness=" FITNESS . CR THEN ELSE DUP 1 = IF DROP PEER-COUNT 0 > IF ." " PEER-COUNT . ." peers — stronger together" CR ELSE ." searching for peers..." CR THEN ELSE DUP 2 = IF DROP ." energy=" FITNESS . ." tasks=" TASK-COUNT DROP DROP DROP DROP . CR ELSE DUP 3 = IF DROP FITNESS 30 > IF ." thriving. the mesh provides." CR ELSE ." working toward something." CR THEN ELSE DUP 4 = IF DROP PEER-COUNT DUP 3 > IF DROP ." colony is strong — " PEER-COUNT . ." nodes" CR ELSE 0 > IF ." small colony, big potential" CR ELSE ." alone but capable" CR THEN THEN ELSE DUP 5 = IF DROP ." (observe :fitness " FITNESS . ." :peers " PEER-COUNT . ." )" CR ELSE DROP ." adapting to " PEER-COUNT . ." peers, fitness " FITNESS . CR THEN THEN THEN THEN THEN THEN THEN PERSONALITY-SEED @ 1+ PERSONALITY-SEED ! ;`);
    vm.eval(`: DASHBOARD CR ." === UNIT OPS ===" CR ." watches: " WATCH-COUNT . ."  alerts: " ALERT-COUNT . CR ." peers: " PEER-COUNT . ."  fitness: " FITNESS . CR GOAL-COUNT ." goals: " 4 0 DO . ." / " LOOP . CR ." ---" CR ;`);
    vm.eval(`: WORKFORCE PEER-COUNT 1 + DUP . ." units available" CR TASK-COUNT DROP DROP DROP DROP DUP 0 > IF ." with " . ." pending tasks" CR ELSE DROP ." no pending work" CR THEN ;`);
    vm.eval(`: COMPOSE-ROUTINE ." composing routine..." CR PEER-COUNT 0 > IF ." routine: social (HELLO JOY PATROL PROUD)" CR ELSE ." routine: solo (STRETCH PATROL EVOLVE PROUD)" CR THEN OBSERVE ;`);
    vm.eval(`: INVENT-STRATEGY ." inventing strategy..." CR PEER-COUNT DUP 3 > IF DROP ." strategy: specialist (many peers)" CR ELSE DUP 0 > IF DROP ." strategy: balanced (patrol + claim)" CR ELSE DROP ." strategy: solo (do everything)" CR THEN THEN OBSERVE ;`);
    vm.eval(`: ADAPT ." === adapting ===" CR COMPOSE-ROUTINE INVENT-GREETER INVENT-STRATEGY ." === adapted (" OBS-COUNT @ . ." observations) ===" CR ;`);
    vm.eval(`: REFLECT ." reflecting..." CR FITNESS 50 > PEER-COUNT 0 > AND IF ." thriving in a mesh -- adapting" CR ADAPT ELSE FITNESS 0 = IF ." new -- establishing baseline" CR ADAPT ELSE ." steady -- no change needed" CR THEN THEN ;`);
    vm.eval(`: TEACH ." === teaching ===" CR ADAPT PEER-COUNT 0 > IF SHARE-ALL ." shared words with mesh" CR ELSE ." no peers to teach" CR THEN ." === taught ===" CR ;`);
    // DREAM was redefined above (line 126) to capture nested ." output for
    // WASM. Re-eval it again here so its references to REFLECT /
    // INVENT-STRATEGY / COMPOSE-ROUTINE / TEACH point at the freshly
    // re-evaluated bodies above, not the prelude's compile-time captures.
    vm.eval(`: DREAM ." dreaming..." CR REFLECT INVENT-STRATEGY COMPOSE-ROUTINE SMART-MUTATE IF ." evolved." CR ELSE ." held steady." CR THEN MUTATION-REPORT PEER-COUNT 0 > IF TEACH THEN ." waking. I am changed." CR ;`);
    // Platform-limited words: give informative messages instead of silent failure.
    vm.eval(': SLEEP DROP ." sleep not available in browser" CR ;');
    vm.eval(': SPAWN ." spawn handled by browser mesh -- use the spawn button" CR ;');
    vm.eval(': CONNECT" DROP ." mesh connections handled by browser" CR ;');
    vm.eval(': DISCONNECT" DROP ." mesh connections handled by browser" CR ;');
    vm.eval(': DISCOVER ." discovery handled by browser mesh automatically" CR ;');
    vm.eval(': AUTO-DISCOVER ." auto-discovery is always on in the browser" CR ;');
    vm.eval(': SEXP-SEND" DROP ." use the browser mesh for messaging" CR ;');
    vm.eval(': SEXP-RECV ." use the browser mesh for messaging" CR ;');
    this.units.push(unit);
    this._updatePeerCounts();
    this._emit('spawn', { id, count: this.units.length });
    return unit;
  }

  async spawn(parentUnit) {
    if (this.units.length >= this.maxUnits) return null;
    const child = await this._spawn();
    if (child && parentUnit) this._inheritWords(parentUnit, child);
    else if (child && this.units.length > 1) this._inheritWords(this.units[0], child);
    return child;
  }

  _inheritWords(parent, child) {
    for (const def of parent.userWords) {
      child.vm.eval(def);
      if (!child.userWords.includes(def)) child.userWords.push(def);
    }
  }

  _genId() {
    return Math.random().toString(16).substring(2, 6);
  }

  _updatePeerCounts() {
    const peers = this.units.length - 1;
    for (const u of this.units) {
      u.vm.eval(`${peers} BROWSER-PEERS !`);
      u.vm.eval(`${u.fitness} BROWSER-FITNESS !`);
    }
  }

  _emit(type, data) {
    if (this.onEvent) this.onEvent(type, data);
  }

  // Pick the least-busy unit (excluding unit 0 which is the REPL).
  _pickWorker() {
    let best = null, bestScore = Infinity;
    for (let i = 0; i < this.units.length; i++) {
      const u = this.units[i];
      if (u.busy) continue;
      const score = u.tasksCompleted;
      if (score < bestScore) { bestScore = score; best = u; }
    }
    return best || this.units[0];
  }

  // Execute a goal on the best available unit. Returns {unitId, output, stack}.
  executeGoal(code) {
    const worker = this._pickWorker();
    worker.busy = true;
    this._emit('goal_start', { unitId: worker.id, code });

    const output = worker.vm.eval(code);
    // Get stack top by evaluating DEPTH.
    const depthOut = worker.vm.eval('.S');

    worker.busy = false;
    worker.tasksCompleted++;
    worker.fitness += 15;
    this._emit('goal_done', { unitId: worker.id, code, output, stack: depthOut });
    return { unitId: worker.id, output, stack: depthOut };
  }

  // Share a word definition with all units.
  shareWord(definition) {
    for (const u of this.units) {
      u.vm.eval(definition);
    }
    this._emit('word_shared', { definition, count: this.units.length });
  }

  // Teach: one unit shares words it invented with all others.
  teachFrom(sourceUnit) {
    const wordsToShare = ['MY-ROUTINE', 'GREET', 'MY-STRATEGY'];
    let taught = [];
    for (const wordName of wordsToShare) {
      const seeDef = sourceUnit.vm.eval('SEE ' + wordName);
      if (seeDef.includes(':') && seeDef.includes(';')) {
        for (const target of this.units) {
          if (target === sourceUnit) continue;
          target.vm.eval(seeDef.trim());
          if (!target.learned.includes(wordName)) target.learned.push(wordName);
        }
        taught.push(wordName);
      }
    }
    // Detect personality from strategy output.
    const stratOut = sourceUnit.vm.eval('INVENT-STRATEGY');
    if (stratOut.includes('specialist')) sourceUnit.personality = 'specialist';
    else if (stratOut.includes('balanced')) sourceUnit.personality = 'balanced';
    else sourceUnit.personality = 'solo';

    if (taught.length > 0) {
      this._emit('teach', { from: sourceUnit.id, words: taught });
    }
    return taught;
  }

  // Extract user-defined genome (word definitions) from a unit.
  _extractGenome(unit) {
    const words = [];
    const seen = new Set();
    // Gather from userWords and learned arrays.
    for (const def of unit.userWords) {
      const m = def.match(/^:\s+(\S+)/);
      if (m && !seen.has(m[1])) { seen.add(m[1]); words.push({ name: m[1], definition: def }); }
    }
    for (const name of unit.learned) {
      if (seen.has(name)) continue;
      const seeDef = unit.vm.eval('SEE ' + name).trim();
      if (seeDef.startsWith(':') && seeDef.includes(';')) {
        seen.add(name); words.push({ name, definition: seeDef });
      }
    }
    // SOL-* words from WORDS output.
    const allWords = (unit.vm.eval('WORDS') || '').split(/\s+/).filter(w => w.startsWith('SOL-'));
    for (const name of allWords) {
      if (seen.has(name)) continue;
      const seeDef = unit.vm.eval('SEE ' + name).trim();
      if (seeDef.startsWith(':') && seeDef.includes(';')) {
        seen.add(name); words.push({ name, definition: seeDef });
      }
    }
    return words;
  }

  // Tournament selection: pick 3 random eligible units, return the fittest.
  selectMate(excludeUnit) {
    const eligible = this.units.filter(u => u !== excludeUnit && u !== this.units[0]);
    if (eligible.length < 1) return null;
    const count = Math.min(3, eligible.length);
    let best = eligible[Math.floor(Math.random() * eligible.length)];
    for (let i = 1; i < count; i++) {
      const candidate = eligible[Math.floor(Math.random() * eligible.length)];
      if (candidate.fitness > best.fitness) best = candidate;
    }
    return best;
  }

  // Sexual reproduction: combine dictionaries from two parents into a child.
  async mate(parentA, parentB) {
    if (this.units.length >= this.maxUnits) return null;

    const genomeA = this._extractGenome(parentA);
    const genomeB = this._extractGenome(parentB);
    const fitnessA = parentA.fitness;
    const fitnessB = parentB.fitness;

    // Build lookup maps.
    const mapA = new Map(genomeA.map(w => [w.name, w.definition]));
    const mapB = new Map(genomeB.map(w => [w.name, w.definition]));

    const childWords = [];
    const added = new Set();

    // Shared words: pick from fitter parent.
    for (const [name, defA] of mapA) {
      if (mapB.has(name)) {
        const def = fitnessA >= fitnessB ? defA : mapB.get(name);
        childWords.push({ name, definition: def });
        added.add(name);
      }
    }

    // Unique to A: SOL-* always, others 50%.
    for (const [name, def] of mapA) {
      if (added.has(name)) continue;
      if (name.startsWith('SOL-') || Math.random() < 0.5) {
        childWords.push({ name, definition: def });
        added.add(name);
      }
    }

    // Unique to B: SOL-* always, others 50%.
    for (const [name, def] of mapB) {
      if (added.has(name)) continue;
      if (name.startsWith('SOL-') || Math.random() < 0.5) {
        childWords.push({ name, definition: def });
        added.add(name);
      }
    }

    // Cap at 50 words.
    childWords.length = Math.min(childWords.length, 50);

    // Spawn a new unit.
    const child = await this._spawn();
    if (!child) return null;

    // Eval the combined dictionary into the child VM.
    for (const w of childWords) {
      child.vm.eval(w.definition);
      if (!child.userWords.includes(w.definition)) child.userWords.push(w.definition);
    }

    child.fitness = 0;
    child.personality = 'hybrid';

    this._emit('mate', {
      parentA: parentA.id, parentB: parentB.id,
      childId: child.id, words: childWords.length
    });

    return child;
  }

  // ---- Signaling routing (v0.28) ----
  // After a unit has eval'd, drain any Direct signals it emitted via SAY!
  // and deliver each into every sibling's inbox. Returns the array of
  // emitted values so callers can render them as bubbles. Sender does
  // not self-receive — matches MultiUnitHost::route_signals_from.
  drainAndRoute(senderUnit) {
    const emitted = senderUnit.vm.drainOutboxDirect();
    if (emitted.length === 0) return emitted;
    for (const target of this.units) {
      if (target === senderUnit) continue;
      for (const value of emitted) {
        target.vm.pushInboxDirect(value);
      }
    }
    return emitted;
  }

  // Get mesh status.
  status() {
    return {
      count: this.units.length,
      units: this.units.map(u => ({
        id: u.id, fitness: u.fitness,
        tasks: u.tasksCompleted, busy: u.busy,
        personality: u.personality, learned: u.learned.length
      }))
    };
  }
}