@homie-rocks/studio 0.24.3 → 0.24.5
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 +51 -1
- package/README.md +21 -5
- package/agents/GUIDES.md +13 -6
- package/bin/homie-studio.mjs +34 -10
- package/chat/CHAT.md +38 -5
- package/lib/agent-seat.mjs +60 -2
- package/lib/brain-probe.mjs +6 -5
- package/lib/build.mjs +6 -0
- package/lib/cloudflare.mjs +2 -0
- package/lib/doctor.mjs +14 -0
- package/lib/local-ai.mjs +66 -0
- package/lib/mcp-tools.mjs +10 -2
- package/lib/scaffold.mjs +17 -4
- package/lib/servers.mjs +31 -2
- package/lib/template-history.json +78 -0
- package/netplay/NETPLAY.md +95 -17
- package/netplay/netplay.ts +82 -0
- package/package.json +1 -1
- package/site/SITE.md +4 -0
- package/starters/ember-vale/game.json +1 -0
- package/starters/ember-vale/src/main.ts +80 -6
- package/worker/agents.mjs +93 -47
- package/worker/brain.mjs +356 -5
- package/worker/chat-page.mjs +173 -28
- package/worker/chat.mjs +17 -7
- package/worker/index.mjs +57 -2
- package/worker/office.mjs +63 -3
- package/worker/room.mjs +48 -3
- package/worker/version.mjs +1 -1
package/worker/room.mjs
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* =============================================================================
|
|
45
45
|
*/
|
|
46
46
|
import { aiName, skillPreset, stripAi } from './agents.mjs';
|
|
47
|
-
import { checkArgs, talks, vocabularyOf } from './brain.mjs';
|
|
47
|
+
import { DECIDE, checkArgs, checkDecide, talks, vocabularyOf } from './brain.mjs';
|
|
48
48
|
import { CHAT_LIMITS, CHAT_RATES, HELD_WORDS, allows, cleanText, floor, normalizeChat, publicChat } from './chat.mjs';
|
|
49
49
|
|
|
50
50
|
export const NET_VERSION = 1;
|
|
@@ -103,6 +103,8 @@ export const LIMITS = Object.freeze({
|
|
|
103
103
|
hello: 2048, snap: 16384, in: 2048, ev: 4096, ckpt: 65536, state: 8192, round: 8192, roster: 4096, ping: 256, other: 512,
|
|
104
104
|
// Room chat (revision 8): a typed line (280 characters, emoji count double), a reaction.
|
|
105
105
|
say: 1536, react: 256,
|
|
106
|
+
// A game's own decision (section 20): a few typed questions about its state.
|
|
107
|
+
decide: DECIDE.bytes,
|
|
106
108
|
});
|
|
107
109
|
/**
|
|
108
110
|
* What grows with the seats (every body, every result, every slot): the checkpoint, round and roster caps double
|
|
@@ -112,7 +114,7 @@ export const LIMITS = Object.freeze({
|
|
|
112
114
|
export const GROWS = Object.freeze(['ckpt', 'round', 'roster']);
|
|
113
115
|
export const capOf = (t, seats) => (LIMITS[t] ?? LIMITS.other) * (GROWS.includes(t) && seats > 16 ? 2 : 1);
|
|
114
116
|
/** Rate caps (messages per rolling second, per client). Over the cap a message is dropped, counted and reported. */
|
|
115
|
-
export const RATES = Object.freeze({ snap: 30, in: 60, ev: 30, ckpt: 4, state: 64, round: 4, roster: 8, ping: 8, other: 8, say: 4, react: 10 });
|
|
117
|
+
export const RATES = Object.freeze({ snap: 30, in: 60, ev: 30, ckpt: 4, state: 64, round: 4, roster: 8, ping: 8, other: 8, say: 4, react: 10, decide: 2 });
|
|
116
118
|
/** `ev` per second by the sender's role: a screen is a spectator, and the host is somebody's phone. */
|
|
117
119
|
export const EV_RATES = Object.freeze({ host: 30, replica: 10, screen: 2 });
|
|
118
120
|
/** The keyed state channel: at most this many keys and bytes per room. */
|
|
@@ -274,7 +276,7 @@ export class NetRoom {
|
|
|
274
276
|
this.stats = {
|
|
275
277
|
snaps: 0, ins: 0, evs: 0, ckpts: 0, states: 0, rounds: 0, drops: 0, oversize: 0, stFixed: 0, reclaimed: 0, seated: 0,
|
|
276
278
|
kicked: 0, refused: 0, persists: 0, restoredFrom: null, bytesIn: 0, bytesOut: 0, elections: [], promotions: 0,
|
|
277
|
-
speechDrops: 0, agentDrops: 0, labelled: 0, votes: 0,
|
|
279
|
+
speechDrops: 0, agentDrops: 0, labelled: 0, votes: 0, decides: 0, decided: {}, decideMs: [],
|
|
278
280
|
};
|
|
279
281
|
this.snapTimes = [];
|
|
280
282
|
/** The owner's controls (section 15): who is held out of this room until when, who is muted, the banner, a closed door. */
|
|
@@ -299,6 +301,13 @@ export class NetRoom {
|
|
|
299
301
|
this.chatReacts = [];
|
|
300
302
|
this.chatPending = 0;
|
|
301
303
|
this.review = null;
|
|
304
|
+
/**
|
|
305
|
+
* A game's own decisions (section 20): the host's typed questions, answered by the studio's decision model (the Table
|
|
306
|
+
* sets `decider`; null: the host's floor answers), at most one every 3 s and 20 a minute a room, two at a time.
|
|
307
|
+
*/
|
|
308
|
+
this.decider = null;
|
|
309
|
+
this.decideTimes = [];
|
|
310
|
+
this.decidePending = 0;
|
|
302
311
|
this.watchIds = new WeakMap();
|
|
303
312
|
this.defaultChat = null;
|
|
304
313
|
}
|
|
@@ -403,6 +412,39 @@ export class NetRoom {
|
|
|
403
412
|
peers() { return this.live().map((c) => this.peer(c)); }
|
|
404
413
|
stateObject() { const o = {}; for (const [k, v] of this.state) o[k] = v.d; return o; }
|
|
405
414
|
|
|
415
|
+
/**
|
|
416
|
+
* A game's own decision (section 20): the host's typed questions about its state, answered by the studio's decision
|
|
417
|
+
* model through the Table (`decider`). Only the host asks; the answer (`decided`, option ids and numbers only) goes
|
|
418
|
+
* back to that socket alone. Off, paced, over budget, too slow or wrong: `ok: false` with why, and the host's own floor
|
|
419
|
+
* answers. `n` is the host's own id for the request.
|
|
420
|
+
*/
|
|
421
|
+
onDecide(c, m, isHost) {
|
|
422
|
+
const n = typeof m.n === 'string' || Number.isInteger(m.n) ? String(m.n).slice(0, 16) : '';
|
|
423
|
+
const reply = (o) => this.send(c, { t: 'decided', n, ...o });
|
|
424
|
+
if (!isHost || this.lite(c)) return reply({ ok: false, why: 'not-host' });
|
|
425
|
+
if (typeof this.decider !== 'function') return reply({ ok: false, why: 'off' });
|
|
426
|
+
const now = this.now();
|
|
427
|
+
while (this.decideTimes.length && this.decideTimes[0] <= now - 60_000) this.decideTimes.shift();
|
|
428
|
+
const last = this.decideTimes.at(-1) ?? -Infinity;
|
|
429
|
+
if (now - last < DECIDE.gapMs - 100 || this.decideTimes.length >= DECIDE.perMinute) return reply({ ok: false, why: 'pace', retryMs: Math.max(DECIDE.gapMs - (now - last), this.decideTimes.length >= DECIDE.perMinute ? this.decideTimes[0] + 60_000 - now : 0) });
|
|
430
|
+
if (this.decidePending >= DECIDE.inFlight) return reply({ ok: false, why: 'busy' });
|
|
431
|
+
const q = checkDecide(m.state, m.questions);
|
|
432
|
+
if (!q.ok) return reply({ ok: false, why: 'bad', message: q.why });
|
|
433
|
+
this.decideTimes.push(now);
|
|
434
|
+
this.decidePending += 1;
|
|
435
|
+
this.stats.decides += 1;
|
|
436
|
+
Promise.resolve()
|
|
437
|
+
.then(() => this.decider(q.state, q.questions, { room: this.code, kids: this.policy.kids }))
|
|
438
|
+
.then((r) => {
|
|
439
|
+
const by = r?.ok ? r.by ?? 'ai' : r?.why ?? 'error';
|
|
440
|
+
this.stats.decided[by] = (this.stats.decided[by] ?? 0) + 1;
|
|
441
|
+
if (r?.ok) { this.stats.decideMs.push(r.ms ?? 0); if (this.stats.decideMs.length > 40) this.stats.decideMs.shift(); this.stats.decideNeurons = (this.stats.decideNeurons ?? 0) + (Number(r.neurons) || 0); }
|
|
442
|
+
reply(r?.ok ? { ok: true, by: r.by ?? 'ai', picks: r.picks ?? {}, p: r.p ?? {}, ms: r.ms ?? null } : { ok: false, why: r?.why ?? 'error' });
|
|
443
|
+
})
|
|
444
|
+
.catch(() => { this.stats.decided.error = (this.stats.decided.error ?? 0) + 1; reply({ ok: false, why: 'error' }); })
|
|
445
|
+
.finally(() => { this.decidePending -= 1; });
|
|
446
|
+
}
|
|
447
|
+
|
|
406
448
|
/** Drop the socket from the room and close it. */
|
|
407
449
|
kick(c, why, code = 1008) {
|
|
408
450
|
this.stats.kicked += 1;
|
|
@@ -515,6 +557,7 @@ export class NetRoom {
|
|
|
515
557
|
}
|
|
516
558
|
return;
|
|
517
559
|
}
|
|
560
|
+
case 'decide': return this.onDecide(c, m, isHost);
|
|
518
561
|
case 'ckpt': {
|
|
519
562
|
if (!isHost) return;
|
|
520
563
|
this.lastCkpt = { k: Number(m.k) || 0, st: this.stamp(m.st), d: m.d ?? null, ...(Array.isArray(m.c) ? { c: m.c.slice(0, 64) } : {}) };
|
|
@@ -1821,6 +1864,8 @@ export class NetRoom {
|
|
|
1821
1864
|
pending: this.chatPending,
|
|
1822
1865
|
lines: this.recentChat(now).map((r) => ({ ...this.wireLine(r), kind: r.kind, client: r.from?.client ?? null, player: r.from?.player ?? null, browser: browserTag(r.from?.browser), ...(r.review ? { review: r.review } : {}) })),
|
|
1823
1866
|
},
|
|
1867
|
+
// A game's own decisions (section 20): how many, who answered, the model's time and neurons (never a state).
|
|
1868
|
+
...(this.stats.decides ? { decides: { n: this.stats.decides, by: { ...this.stats.decided }, msP50: [...this.stats.decideMs].sort((x, y) => x - y)[Math.floor(this.stats.decideMs.length / 2)] ?? null, neurons: Math.round((this.stats.decideNeurons ?? 0) * 100) / 100 } } : {}),
|
|
1824
1869
|
},
|
|
1825
1870
|
};
|
|
1826
1871
|
}
|
package/worker/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The @homie-rocks/studio version this Worker code is (package.json's; a test keeps them equal). Cache-busts /_homie/site.js. */
|
|
2
|
-
export const STUDIO_VERSION_TAG = '0.24.
|
|
2
|
+
export const STUDIO_VERSION_TAG = '0.24.5';
|