oxedyne/daimond/dev/verify_wire.mjs
28.6 KiB, 1 run
created by r2519314175:813, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // verify_wire.mjs — the Wire band says what actually goes to the model, on BOTH |
| 2 | // threads. |
| 3 | // |
| 4 | // The Wire is a chip in the chat header that opens the invisible part of every |
| 5 | // request: the composed system message split by whose each paragraph is, and the |
| 6 | // tool schemas beside it. Its whole selling point, written into its own commit, |
| 7 | // is that it CANNOT DRIFT from the request, because one function composes what |
| 8 | // goes out and what is shown. |
| 9 | // |
| 10 | // That was true of a chat and false of a Diamond. A daimon's turn never goes |
| 11 | // through `run_turn`: `sendUserMessage` hands a record carrying a `diamondId` to |
| 12 | // `doSteer`, which steers the DIAMOND'S OWN app through `steer_crystal`, and |
| 13 | // `steer_inner` builds its own tool vector — seventeen tools, none of the nine |
| 14 | // web tools and no `file_show` for a worker. `renderWire` read |
| 15 | // `ensureApp(current)`, an ordinary CHAT app whose registry is `Tool::browser()`: |
| 16 | // twenty-eight tools. So a Diamond's owner was shown a toolbelt his daimon has |
| 17 | // never held, while he sat asking why it never used one of them. |
| 18 | // |
| 19 | // `say` used to be the tool this file told the two belts apart by. It is gone — |
| 20 | // an answer is written at two depths in the model's own prose now — so the |
| 21 | // distinguishing tool is `file_show`, and `say` is asserted ABSENT from both. |
| 22 | // |
| 23 | // THE ORACLE IS THE MOCK PROVIDER, not the app's opinion of itself. Every check |
| 24 | // below compares the band on screen with the request the model actually received |
| 25 | // — `dev/mockllm.mjs` logs the whole payload, tool names and all — so a band and |
| 26 | // an engine that agree with each other and not with the wire cannot both pass. |
| 27 | // |
| 28 | // Four properties: |
| 29 | // |
| 30 | // 1. On an ORDINARY CHAT the band's tool list is the chat's real registry, and |
| 31 | // its system message is the string the provider was sent. |
| 32 | // 2. On a DAIMON THREAD the band's tool list is the DAIMON'S real registry — |
| 33 | // and carries no web tool, because the daimon has not got them. This is the |
| 34 | // check the shipped feature failed. Neither belt carries `say` at all. |
| 35 | // 3. The token figures the band reports for a daimon are the daimon's: the |
| 36 | // schema count is the daimon's tool count, the total is not the chat's, and |
| 37 | // it is the arithmetic the app itself does over the daimon's own bands. |
| 38 | // 4. The per-turn paragraph — this Diamond's folder, its attachments and its |
| 39 | // crystal — is drawn under its own heading and has a bounding rectangle. |
| 40 | // Measured as an area, never as a computed `display`: `display:none` does |
| 41 | // not cascade, so a parent that is gone leaves a child still "block". |
| 42 | // |
| 43 | // node dev/verify_wire.mjs |
| 44 | // node dev/verify_wire.mjs --break chatapp # the daimon thread reads the chat app again |
| 45 | // node dev/verify_wire.mjs --break dropshow # the band drops one tool from its list |
| 46 | // node dev/verify_wire.mjs --break splice # a band spliced out of two distant pieces |
| 47 | // node dev/verify_wire.mjs --break pretty # the schemas counted as the band prints them |
| 48 | // node dev/verify_wire.mjs --break chars # the schemas counted in characters, not bytes |
| 49 | // |
| 50 | // Needs dev/serve.mjs and dev/mockllm.mjs (dev/world.sh N --up gives both). |
| 51 | import fs from 'node:fs'; |
| 52 | import path from 'node:path'; |
| 53 | import os from 'node:os'; |
| 54 | import { fileURLToPath } from 'node:url'; |
| 55 | import { open, chat, steerDiamond, scratch, shot, mockLog, clearMockLog, contentText } from './harness.mjs'; |
| 56 | import { whyStaleWasm, refuse } from './staleguard.mjs'; |
| 57 | |
| 58 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 59 | const ROOT = path.join(HERE, '..'); |
| 60 | const OUT = path.join(os.homedir(), '.cache/daimond/wire-shots'); |
| 61 | fs.mkdirSync(OUT, { recursive: true }); |
| 62 | |
| 63 | // The composition under test is in the wasm — `wire_json`, and the |
| 64 | // `compose_daimon` both the turn and the band read — so a stale bundle would |
| 65 | // measure the defect this run exists to disprove. |
| 66 | refuse(whyStaleWasm(path.join(ROOT, 'www/pkg/oxedyne_daimond_bg.wasm'), path.join(ROOT, 'src'), { |
| 67 | subject: 'The Wire\'s composition', |
| 68 | holds: '`wire_json` and `compose_daimon`, which the band and the turn share', |
| 69 | })); |
| 70 | |
| 71 | const BI = process.argv.indexOf('--break'); |
| 72 | const BEQ = process.argv.find(a => a.startsWith('--break=')); |
| 73 | const BREAK = BEQ ? BEQ.split('=')[1] : (BI >= 0 ? (process.argv[BI + 1] || '') : ''); |
| 74 | |
| 75 | const HOUSE = 'Answer in the fewest words that are still true. RANUNCULUS.'; |
| 76 | |
| 77 | let failures = 0; |
| 78 | const check = (cond, msg, detail) => { |
| 79 | console.log((cond ? ' ok ' : ' FAIL ') + msg + (detail != null ? ' — ' + detail : '')); |
| 80 | if (!cond) failures++; |
| 81 | }; |
| 82 | |
| 83 | // ── The breaks ─────────────────────────────────────────────────────────── |
| 84 | // |
| 85 | // Served, not applied to the tree: the file on disk is never touched, so a run |
| 86 | // that dies half way leaves nothing behind to confuse the next one. |
| 87 | const WWW = path.join(ROOT, 'www'); |
| 88 | const BREAKS = { |
| 89 | // The defect exactly as it shipped: the band composed from an ordinary chat |
| 90 | // app, with no Diamond named, on a thread that steers a daimon. |
| 91 | chatapp: [ |
| 92 | { file: 'js/daimond.js', |
| 93 | find: "\t\ttry { app = did ? diamondApp(did) : ensureApp(current); } catch (e) { return; }", |
| 94 | with: "\t\ttry { app = ensureApp(current); } catch (e) { return; }" }, |
| 95 | { file: 'js/daimond.js', |
| 96 | find: "\t\t\tw = JSON.parse(await app.wire_system(did,", |
| 97 | with: "\t\t\tw = JSON.parse(await app.wire_system('',", }, |
| 98 | ], |
| 99 | // The user's own house rules left inside the safety band, which is what a |
| 100 | // first cut of this fix did: harmless on a chat, where the two are adjacent, |
| 101 | // and on a Diamond a band made of two pieces with a paragraph missing from |
| 102 | // between them -- text that never stood together in the message. |
| 103 | splice: [ |
| 104 | { file: 'js/daimond.js', |
| 105 | find: "\t\tvar std = role.indexOf('## Standing instructions from the user');\n" |
| 106 | + "\t\tif (std > 0) { standing = role.slice(std); role = role.slice(0, std); }", |
| 107 | with: "\t\tvar std = -1;" }, |
| 108 | ], |
| 109 | // The schemas counted in the form the band DRAWS rather than the form the |
| 110 | // request carries. This is the defect as it shipped: the figure people quote |
| 111 | // from this band -- "9k of the 11k is tool schemas" -- overstated by an eighth, |
| 112 | // entirely in whitespace the viewer added itself. |
| 113 | pretty: [ |
| 114 | { file: 'js/daimond.js', |
| 115 | find: "\t\tvar schemaTok = Math.round((w.schemas_len || 0) / 4);", |
| 116 | with: "\t\tvar schemaTok = wireTok(schemas);" }, |
| 117 | ], |
| 118 | // The schemas counted in CHARACTERS rather than in the bytes the body carries. Not the |
| 119 | // same mistake as `pretty`: this copy is the compact array, the one the request really |
| 120 | // holds, measured with `String.length` -- UTF-16 code units, so 41,964 where the body |
| 121 | // carries 41,998. Thirty-four bytes, and on 2026-08-25 they straddled the 10,500-token |
| 122 | // rounding step, so the band drew "11k" and a run measuring the same array in characters |
| 123 | // drew "10k" and reported the band as having drifted from the request. It had not. |
| 124 | chars: [ |
| 125 | { file: 'js/daimond.js', |
| 126 | find: "\t\tvar schemaTok = Math.round((w.schemas_len || 0) / 4);", |
| 127 | with: "\t\tvar schemaTok = Math.round(JSON.stringify(w.schemas || []).length / 4);" }, |
| 128 | ], |
| 129 | // One tool quietly missing from what the band draws, and present in the |
| 130 | // request. Here to prove check 1 has teeth: a check that only ever reads a |
| 131 | // list that is right proves nothing about a list that is wrong. |
| 132 | dropshow: [ |
| 133 | { file: 'js/daimond.js', |
| 134 | find: "\t\tvar schemas = JSON.stringify(w.schemas || [], null, 1);", |
| 135 | with: "\t\tvar schemas = JSON.stringify((w.schemas || []).filter(function (d) " |
| 136 | + "{ return ((d || {}).function || {}).name !== 'file_show'; }), null, 1);" }, |
| 137 | ], |
| 138 | }; |
| 139 | |
| 140 | if (BREAK && !BREAKS[BREAK]) { |
| 141 | console.error(`unknown break '${BREAK}'; one of: ${Object.keys(BREAKS).join(', ')}`); |
| 142 | process.exit(2); |
| 143 | } |
| 144 | |
| 145 | /// Every edit a break makes to ONE file, applied to one copy of it. |
| 146 | /// |
| 147 | /// Grouped by file because `page.route` keeps only the LAST handler registered |
| 148 | /// for a pattern: two specs against `js/daimond.js` registered separately would |
| 149 | /// serve the second edit alone, and the run would report a check as proved that |
| 150 | /// had been asked of working code. |
| 151 | function damaged(file, specs) { |
| 152 | let src = fs.readFileSync(path.join(WWW, file), 'utf8'); |
| 153 | for (const spec of specs) { |
| 154 | const n = src.split(spec.find).length - 1; |
| 155 | if (n !== 1) { |
| 156 | console.error(`break '${BREAK}': the anchor appears ${n} times in ${file}, ` |
| 157 | + 'so nothing was broken and the run below would prove nothing.'); |
| 158 | process.exit(2); |
| 159 | } |
| 160 | src = src.replace(spec.find, spec.with); |
| 161 | } |
| 162 | return src; |
| 163 | } |
| 164 | |
| 165 | /// Keep the RAW body of every request to the provider. |
| 166 | /// |
| 167 | /// The mock logs the payload it parsed, which is enough for names and messages |
| 168 | /// and not for a SIZE: a re-serialised copy is a second opinion about the very |
| 169 | /// bytes in question. The string handed to `fetch` is the request, so it is kept |
| 170 | /// as a string and measured as one. |
| 171 | async function watchWire(page) { |
| 172 | await page.addInitScript(() => { |
| 173 | // The engine builds a `Request` and hands THAT to `fetch`, so a hook on `fetch` |
| 174 | // alone sees a body that is already a stream. The constructor is where the |
| 175 | // string still exists. |
| 176 | const keep = (url, init) => { |
| 177 | try { |
| 178 | if (/chat\/completions/.test(String(url || '')) && init && typeof init.body === 'string') { |
| 179 | window.__wireRaw = init.body; |
| 180 | } |
| 181 | } catch (e) { /* never break a turn to watch one */ } |
| 182 | }; |
| 183 | window.Request = new Proxy(window.Request, { |
| 184 | construct(target, args) { |
| 185 | const u = args[0]; |
| 186 | keep(typeof u === 'string' ? u : ((u && u.url) || ''), args[1]); |
| 187 | return new target(...args); |
| 188 | }, |
| 189 | }); |
| 190 | const orig = window.fetch; |
| 191 | window.fetch = function (u, init) { |
| 192 | keep(typeof u === 'string' ? u : ((u && u.url) || ''), init); |
| 193 | return orig.apply(this, arguments); |
| 194 | }; |
| 195 | }); |
| 196 | } |
| 197 | |
| 198 | async function serveBreak(page) { |
| 199 | await watchWire(page); |
| 200 | if (!BREAK) return; |
| 201 | const byFile = new Map(); |
| 202 | for (const spec of BREAKS[BREAK]) { |
| 203 | if (!byFile.has(spec.file)) byFile.set(spec.file, []); |
| 204 | byFile.get(spec.file).push(spec); |
| 205 | } |
| 206 | for (const [file, specs] of byFile) { |
| 207 | const body = damaged(file, specs); |
| 208 | await page.route('**/' + file, r => r.fulfill({ |
| 209 | status: 200, contentType: 'application/javascript', body })); |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | // ── Reading the two sides ──────────────────────────────────────────────── |
| 214 | |
| 215 | /// The band on screen, in pieces. |
| 216 | /// |
| 217 | /// The bodies are read as text whether the band is folded open or not — a |
| 218 | /// `<pre>` with `display:none` still carries its `textContent` — but every |
| 219 | /// question about whether something is DRAWN is answered with an area. |
| 220 | const wireDom = (p) => p.evaluate(() => { |
| 221 | const box = document.getElementById('wire-head'); |
| 222 | if (!box) return null; |
| 223 | const rect = (el) => { |
| 224 | const r = el.getBoundingClientRect(); |
| 225 | return Math.round(r.width * r.height); |
| 226 | }; |
| 227 | const bands = [...box.querySelectorAll('.wire-band')].map((row) => { |
| 228 | const head = row.querySelector('.wire-band-head'); |
| 229 | const body = row.querySelector('.wire-band-body'); |
| 230 | return { |
| 231 | name: head.querySelector('.wire-band-name').textContent.replace(/^[▸▾]\s*/, ''), |
| 232 | why: head.querySelector('.wire-band-why').textContent, |
| 233 | tok: head.querySelector('.wire-band-tok').textContent, |
| 234 | text: body ? body.textContent : '', |
| 235 | area: rect(head), |
| 236 | }; |
| 237 | }); |
| 238 | return { area: rect(box), title: (box.querySelector('.wire-title') || {}).textContent || '', bands }; |
| 239 | }); |
| 240 | |
| 241 | // The schema band's heading carries its count -- `Tool schemas (17)` -- so a name |
| 242 | // is matched whole or up to its bracket. |
| 243 | const band = (w, name) => (w |
| 244 | ? w.bands.find(b => b.name === name || b.name.startsWith(name + ' (')) |
| 245 | : null); |
| 246 | |
| 247 | /// The tool names the band draws, read out of the schema band it shows. |
| 248 | /// |
| 249 | /// From the SCHEMAS and not from the tool sentence, deliberately: the schemas are |
| 250 | /// the bytes on the wire, and the sentence is prose about them. |
| 251 | function bandTools(w) { |
| 252 | const b = band(w, 'Tool schemas'); |
| 253 | if (!b) return null; |
| 254 | try { |
| 255 | return JSON.parse(b.text).map(d => (d.function || {}).name).filter(Boolean).sort(); |
| 256 | } catch (e) { return null; } |
| 257 | } |
| 258 | |
| 259 | /// The last request the provider received, and the system message in it. |
| 260 | const lastReq = () => { const r = mockLog(); return r.length ? r[r.length - 1] : null; }; |
| 261 | const sysOf = (row) => { |
| 262 | const m = (row.messages || []).find(x => x.role === 'system'); |
| 263 | return m ? contentText(m.content) : ''; |
| 264 | }; |
| 265 | |
| 266 | /// The app's own arithmetic, so a figure can be checked rather than admired. |
| 267 | /// |
| 268 | /// BYTES, because that is what the band claims to be reporting and what the engine budgets |
| 269 | /// with. `String.length` is UTF-16 code units: for the browser toolbelt it says 41,964 where |
| 270 | /// the body carries 41,998, and on 2026-08-25 those two straddled the 10,500-token rounding |
| 271 | /// step and drew "10k" and "11k". This run then reported a band that had drifted from the |
| 272 | /// request, and it had not -- the two figures were the same array in two encodings. Measuring |
| 273 | /// characters and calling them bytes is the defect this file exists to catch, made by the |
| 274 | /// file itself. |
| 275 | const ENC = new TextEncoder(); |
| 276 | const bytes = (s) => ENC.encode(String(s || '')).length; |
| 277 | const tok = (s) => Math.round(bytes(s) / 4); |
| 278 | const fmtTok = (n) => (n % 1024 === 0) ? (n / 1024) + 'k' |
| 279 | : (n >= 1e6 ? (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M' |
| 280 | : (n >= 1000 ? Math.round(n / 1000) + 'k' : '' + n)); |
| 281 | |
| 282 | /// Turn the band on and wait for it to be drawn. |
| 283 | async function showWire(p) { |
| 284 | const on = await p.evaluate(() => document.getElementById('wire-btn').getAttribute('aria-pressed')); |
| 285 | if (on !== 'true') await p.click('#wire-btn', { force: true }); |
| 286 | await p.waitForTimeout(900); |
| 287 | } |
| 288 | |
| 289 | /// Redraw the band for whatever thread is on screen now. |
| 290 | /// |
| 291 | /// Off and on again rather than trusting a redraw: the band is rebuilt on every |
| 292 | /// thread change, and this run wants the one for the thread it is looking at, |
| 293 | /// not whichever render happened to finish last. |
| 294 | async function redrawWire(p) { |
| 295 | await p.evaluate(() => { |
| 296 | const b = document.getElementById('wire-btn'); |
| 297 | if (b.getAttribute('aria-pressed') === 'true') b.click(); |
| 298 | }); |
| 299 | await p.waitForTimeout(300); |
| 300 | await p.click('#wire-btn', { force: true }); |
| 301 | await p.waitForTimeout(1200); |
| 302 | } |
| 303 | |
| 304 | async function makeDiamond(p, name) { |
| 305 | await p.evaluate(() => document.getElementById('new-diamond-btn').click()); |
| 306 | await p.waitForSelector('.dlg-card', { timeout: 8000 }); |
| 307 | await p.evaluate((nm) => { |
| 308 | const card = [...document.querySelectorAll('.dlg-card')] |
| 309 | .filter(c => c.getClientRects().length).pop(); |
| 310 | const inp = card.querySelector('input.dlg-input'); |
| 311 | inp.value = nm; |
| 312 | inp.dispatchEvent(new Event('input', { bubbles: true })); |
| 313 | card.querySelector('.dlg-ok').click(); |
| 314 | }, name); |
| 315 | await p.waitForTimeout(1400); |
| 316 | } |
| 317 | |
| 318 | // `route` and not a reload afterwards: the damaged file has to be in place before |
| 319 | // the page is navigated, or the module has already been imported by the time the |
| 320 | // route exists and the break is served to nobody. |
| 321 | const s = await open({ name: 'wire', profile: scratch('pw', 'wire-' + process.pid), route: serveBreak }); |
| 322 | const { page: p } = s; |
| 323 | try { |
| 324 | if (BREAK) console.log(` .. running with --break ${BREAK}`); |
| 325 | |
| 326 | // The user's own house rules, in force before either turn. |
| 327 | // |
| 328 | // NOT decoration. They are appended after everything the app composes, so on a |
| 329 | // Diamond they sit on the FAR SIDE of the paragraph this run is about -- and a |
| 330 | // band that lifted that paragraph out without accounting for them would show a |
| 331 | // "Safety clause" made of two pieces that never stood together in the message. |
| 332 | // Seeded through the store, which is where `Instructions.refresh` reads them. |
| 333 | await p.evaluate(async (text) => { |
| 334 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 335 | await m.store_write('DAIMOND.md', text); |
| 336 | await window.DaimondInstructions.refresh(); |
| 337 | }, HOUSE); |
| 338 | await p.waitForTimeout(600); |
| 339 | |
| 340 | // ══ 1. An ordinary chat ═══════════════════════════════════════════ |
| 341 | clearMockLog(); |
| 342 | await chat(s, 'say hello'); |
| 343 | const chatReq = lastReq(); |
| 344 | check(!!chatReq, 'the chat turn reached the provider'); |
| 345 | const chatSent = chatReq ? (chatReq.tools || []).slice().sort() : []; |
| 346 | check(chatSent.includes('file_show') && chatSent.some(n => n.startsWith('web_')), |
| 347 | 'and a chat really does hold `file_show` and the web tools', |
| 348 | `${chatSent.length} tools`); |
| 349 | // The tool that is gone. A chat is the actor `say` was FOR, so if any belt anywhere |
| 350 | // still carries it, this is the one it would be on. |
| 351 | check(!chatSent.includes('say'), |
| 352 | 'and no longer holds `say`, which is not a tool any more', |
| 353 | chatSent.includes('say') ? 'it is still offered one' : ''); |
| 354 | |
| 355 | await showWire(p); |
| 356 | let w = await wireDom(p); |
| 357 | check(!!w && w.area > 0, 'the band is drawn on a chat', w && `area ${w.area}`); |
| 358 | const chatShown = bandTools(w); |
| 359 | check(!!chatShown, 'and its schema band parses as JSON'); |
| 360 | check(JSON.stringify(chatShown) === JSON.stringify(chatSent), |
| 361 | 'THE BAND\'S TOOL LIST IS THE CHAT\'S REAL REGISTRY', |
| 362 | `band ${chatShown ? chatShown.length : '?'} vs wire ${chatSent.length}` |
| 363 | + (JSON.stringify(chatShown) === JSON.stringify(chatSent) ? '' : |
| 364 | `; only in band: [${(chatShown || []).filter(n => !chatSent.includes(n))}]` |
| 365 | + `; only sent: [${chatSent.filter(n => !(chatShown || []).includes(n))}]`)); |
| 366 | |
| 367 | const chatSys = chatReq ? sysOf(chatReq) : ''; |
| 368 | const chatBands = [band(w, 'Role prompt'), band(w, 'Safety clause'), band(w, 'Standing instructions'), |
| 369 | band(w, 'Tool names'), band(w, 'This computer')].filter(Boolean).map(b => b.text); |
| 370 | check(chatBands.length >= 4, 'the chat band has its parts', chatBands.length + ' of them'); |
| 371 | check(chatBands.every(t => t.length > 0 && chatSys.includes(t.trim())), |
| 372 | 'and every one of them is VERBATIM in the system message the provider was sent'); |
| 373 | check(((band(w, 'Standing instructions') || {}).text || '').includes('RANUNCULUS'), |
| 374 | 'the user\'s own house rules are drawn as theirs'); |
| 375 | const chatTitle = w ? w.title : ''; |
| 376 | |
| 377 | // ── The size it reports is the size that goes ──────────────────── |
| 378 | const raw = await p.evaluate(() => window.__wireRaw || ''); |
| 379 | const sentTools = raw ? JSON.parse(raw).tools : null; |
| 380 | const compact = sentTools ? JSON.stringify(sentTools) : ''; |
| 381 | check(!!compact && raw.includes(compact), |
| 382 | 'the compact serialisation IS the engine\'s own bytes, character for character', |
| 383 | compact ? `${bytes(compact)} bytes, found in the body` : 'no body captured'); |
| 384 | const pretty = sentTools ? JSON.stringify(sentTools, null, 1) : ''; |
| 385 | check(bytes(pretty) > bytes(compact) * 1.05, |
| 386 | 'and the printed form is materially bigger, so the two figures are distinguishable', |
| 387 | `${bytes(compact)} sent vs ${bytes(pretty)} printed`); |
| 388 | const schemaBand = band(w, 'Tool schemas') || {}; |
| 389 | check(schemaBand.tok === fmtTok(tok(compact)), |
| 390 | 'THE SCHEMA FIGURE IS THE BYTES THE REQUEST CARRIES', |
| 391 | `band says ${schemaBand.tok}, sent is ${fmtTok(tok(compact))}, ` |
| 392 | + `printed would be ${fmtTok(tok(pretty))}`); |
| 393 | check(schemaBand.tok !== fmtTok(tok(pretty)) || fmtTok(tok(pretty)) === fmtTok(tok(compact)), |
| 394 | 'and not the size of the indenting the band added itself'); |
| 395 | // The chat's headline as a NUMBER, kept for the daimon's half. The headline itself is |
| 396 | // printed to a thousand tokens, and a comparison made on the printed form cannot tell two |
| 397 | // threads apart when their true totals fall inside one rounding step -- see the daimon's |
| 398 | // half below, which is where that cost a red. |
| 399 | const chatTotalTok = tok((band(w, 'Role prompt') || {}).text) + tok((band(w, 'Safety clause') || {}).text) |
| 400 | + tok((band(w, 'Standing instructions') || {}).text) + tok((band(w, 'Tool names') || {}).text) |
| 401 | + tok((band(w, 'This computer') || {}).text) + tok(compact); |
| 402 | // THE THIRD WAY OF BEING WRONG, and the one that fooled this file: the array measured in |
| 403 | // UTF-16 code units. It is neither the indented copy nor the sent bytes -- 41,964 against |
| 404 | // 41,998 for the browser toolbelt -- and the two draw different headlines whenever they |
| 405 | // fall either side of a rounding step, which on 2026-08-25 they did, at 10,500 tokens. |
| 406 | // Asserted apart from the `pretty` check above, so a band that regressed to counting |
| 407 | // characters cannot pass on the strength of merely not being the indented copy. |
| 408 | check(bytes(compact) !== compact.length, |
| 409 | 'the sent array holds multi-byte characters, so bytes and characters CAN disagree', |
| 410 | `${bytes(compact)} bytes over ${compact.length} UTF-16 units`); |
| 411 | // They do not always round apart, and a run that pretended otherwise would be claiming a |
| 412 | // distinction it had not drawn. So it says which kind of run it is. |
| 413 | const charTok = fmtTok(Math.round(compact.length / 4)); |
| 414 | if (charTok === fmtTok(tok(compact))) { |
| 415 | console.log(' .. bytes and characters round alike today (' + charTok |
| 416 | + '), so this run cannot tell those two apart'); |
| 417 | } else { |
| 418 | check(schemaBand.tok !== charTok, |
| 419 | 'and it is not the count of CHARACTERS, which rounds elsewhere today', |
| 420 | `characters would say ${charTok}`); |
| 421 | } |
| 422 | await shot(s, 'chat-wire'); |
| 423 | |
| 424 | // ══ 2. A daimon thread ════════════════════════════════════════════ |
| 425 | await makeDiamond(p, 'Wiremaker'); |
| 426 | const did = await p.evaluate(() => (DaimondDiamond.current() || {}).id || ''); |
| 427 | check(!!did, 'a Diamond is open', did); |
| 428 | clearMockLog(); |
| 429 | // The same path a person takes: the Diamond's chat face, its composer, its send |
| 430 | // button -- which `sendUserMessage` routes to `doSteer` and nowhere near `run_turn`. |
| 431 | await steerDiamond(s, 'note that this happened'); |
| 432 | await p.waitForTimeout(6000); |
| 433 | const dReq = lastReq(); |
| 434 | check(!!dReq, 'the steering turn reached the provider'); |
| 435 | const dSent = dReq ? (dReq.tools || []).slice().sort() : []; |
| 436 | // THE DAIMON HOLDS THE WEB TOOLS NOW, and this check used to say it must not. |
| 437 | // |
| 438 | // `Tool::daimon()` never called `Tool::web()`, so a Diamond built for research held no |
| 439 | // way to search, fetch or read a page while a chat beside it held nine. The owner |
| 440 | // decided on 2026-08-24 that it should, and what stops a tainted turn reaching the |
| 441 | // network is the egress gate rather than withholding the tools -- see the comment above |
| 442 | // the grant in `src/tools.rs` and `dev/verify_daimonreach.mjs`, which holds that half. |
| 443 | // |
| 444 | // `say` is still absent, and that has not changed: it was removed outright, and what it |
| 445 | // used to enforce moved rather than went. |
| 446 | check(dSent.length > 0 && !dSent.includes('say'), |
| 447 | 'and the daimon does not hold `say`, which no longer exists', |
| 448 | `${dSent.length} tools`); |
| 449 | check(dSent.some(n => n.startsWith('web_')), |
| 450 | 'and it DOES hold the web tools, which is the grant of 2026-08-24', |
| 451 | dSent.filter(n => n.startsWith('web_')).join(',') || 'none'); |
| 452 | check(dSent.includes('file_show'), |
| 453 | 'though it does hold `file_show`, so the check above is not simply an empty belt'); |
| 454 | |
| 455 | await redrawWire(p); |
| 456 | w = await wireDom(p); |
| 457 | check(!!w && w.area > 0, 'the band is drawn on a daimon thread', w && `area ${w.area}`); |
| 458 | const dShown = bandTools(w); |
| 459 | check(JSON.stringify(dShown) === JSON.stringify(dSent), |
| 460 | 'THE BAND\'S TOOL LIST IS THE DAIMON\'S REAL REGISTRY', |
| 461 | `band ${dShown ? dShown.length : '?'} vs wire ${dSent.length}` |
| 462 | + (JSON.stringify(dShown) === JSON.stringify(dSent) ? '' : |
| 463 | `; only in band: [${(dShown || []).filter(n => !dSent.includes(n))}]` |
| 464 | + `; only sent: [${dSent.filter(n => !(dShown || []).includes(n))}]`)); |
| 465 | check(!!dShown && !dShown.includes('say'), |
| 466 | 'the band does not offer him `say` on a daimon thread', |
| 467 | dShown && dShown.includes('say') ? 'it does' : ''); |
| 468 | // The band's own half of the grant of 2026-08-24: a daimon holds the web tools, so the |
| 469 | // band must SHOW them. The check above already asserts the band's list is the registry |
| 470 | // character for character; this says which way that agreement now falls, so a band that |
| 471 | // silently stopped drawing them would not pass on the strength of matching an empty belt. |
| 472 | check(!!dShown && dShown.some(n => n.startsWith('web_')), |
| 473 | 'and the band shows the nine web tools the daimon now holds', |
| 474 | dShown ? `[${dShown.filter(n => n.startsWith('web_'))}]` : ''); |
| 475 | |
| 476 | const dSys = dReq ? sysOf(dReq) : ''; |
| 477 | const dParts = [band(w, 'Role prompt'), band(w, 'Safety clause'), band(w, 'This Diamond'), |
| 478 | band(w, 'Standing instructions'), band(w, 'Tool names'), band(w, 'This computer')] |
| 479 | .filter(Boolean).map(b => b.text); |
| 480 | check(dParts.length >= 5, 'the daimon band has its parts', dParts.length + ' of them'); |
| 481 | check(dParts.length > 0 && dParts.every(t => t.length > 0 && dSys.includes(t.trim())), |
| 482 | 'AND EVERY PART OF IT IS IN THE SYSTEM MESSAGE THE PROVIDER WAS SENT', |
| 483 | dParts.map((t, i) => dSys.includes(t.trim()) ? '' : `part ${i} is not`).filter(Boolean).join('; ')); |
| 484 | |
| 485 | // ══ 3. The figures are the daimon's ═══════════════════════════════ |
| 486 | check((dShown || []).length === dSent.length, |
| 487 | 'the daimon\'s schema count is the daimon\'s tool count', |
| 488 | `${(dShown || []).length} vs ${dSent.length}`); |
| 489 | const dTitle = w ? w.title : ''; |
| 490 | // The claim that the daimon's headline is the daimon's own is asserted below, on the |
| 491 | // numbers, once both are in hand. |
| 492 | // The daimon's own schemas, off the daimon's own request. `__wireRaw` holds the |
| 493 | // last body the page sent, which after the steer is the steering turn's. |
| 494 | const dRaw = await p.evaluate(() => window.__wireRaw || ''); |
| 495 | const dCompact = dRaw ? JSON.stringify(JSON.parse(dRaw).tools) : ''; |
| 496 | check(!!dCompact && dRaw.includes(dCompact) && bytes(dCompact) < bytes(compact), |
| 497 | 'the daimon sent its own, smaller, schema array', |
| 498 | `${bytes(dCompact)} bytes against the chat's ${bytes(compact)}`); |
| 499 | const dSchemaTok = tok(dCompact); |
| 500 | check((band(w, 'Tool schemas') || {}).tok === fmtTok(dSchemaTok), |
| 501 | 'and the band reports THAT, in the daimon\'s figure', |
| 502 | `band says ${(band(w, 'Tool schemas') || {}).tok}, sent is ${fmtTok(dSchemaTok)}`); |
| 503 | // The whole headline, against the request rather than against itself: the text |
| 504 | // bands as drawn, plus the schemas as sent. The bands are trimmed at the seams |
| 505 | // where the blob is split, so a few characters go astray either side; the figure |
| 506 | // is printed to a thousand tokens, which no seam can move. |
| 507 | const sysTok = tok((band(w, 'Role prompt') || {}).text) + tok((band(w, 'Safety clause') || {}).text) |
| 508 | + tok((band(w, 'This Diamond') || {}).text) + tok((band(w, 'Standing instructions') || {}).text) |
| 509 | + tok((band(w, 'Tool names') || {}).text) + tok((band(w, 'This computer') || {}).text); |
| 510 | const want = fmtTok(sysTok + dSchemaTok); |
| 511 | check(dTitle.includes(want), |
| 512 | 'and the headline is those bands plus those bytes', |
| 513 | `says "${dTitle}", computed ${want}`); |
| 514 | // AND THE TWO THREADS REALLY DO CARRY DIFFERENT TOTALS, which is what keeps the check |
| 515 | // above from being satisfiable by a headline carried over from the chat. |
| 516 | // |
| 517 | // COMPARED AS NUMBERS, NOT AS THE STRINGS THEY ARE PRINTED AS. This read |
| 518 | // `dTitle !== chatTitle` until 2026-08-25, and `fmtTok` prints anything over a thousand |
| 519 | // tokens to the nearest thousand -- so the moment the daimon's total and the chat's fell |
| 520 | // in the same thousand, two correctly measured figures drew the same sentence and this |
| 521 | // went red about the app. On the gate of that morning both said "about 13k tokens" while |
| 522 | // the arrays behind them were 41,993 and 44,437 bytes: 611 tokens apart, and printed |
| 523 | // identically. A check that cannot tell a collision from a carry-over is measuring the |
| 524 | // rounding, not the band. |
| 525 | const dTotalTok = sysTok + dSchemaTok; |
| 526 | check(!!dTitle && dTotalTok !== chatTotalTok, |
| 527 | 'and it is the daimon\'s own total, not the chat\'s carried across', |
| 528 | `daimon ${dTotalTok} tok "${dTitle}" vs chat ${chatTotalTok} tok "${chatTitle}"`); |
| 529 | |
| 530 | // ══ 4. The per-turn paragraph, drawn ══════════════════════════════ |
| 531 | const local = band(w, 'This Diamond'); |
| 532 | check(!!local, 'this turn\'s own paragraph has a band of its own'); |
| 533 | check(!!local && local.area > 0, 'and it is drawn, measured as an area', |
| 534 | local && `area ${local.area}`); |
| 535 | check(!!local && local.text.includes('diamonds/' + did), |
| 536 | 'it names THIS Diamond\'s folder', local && local.text.slice(0, 90)); |
| 537 | check(!!local && local.text.includes('Current crystal.json:'), |
| 538 | 'and carries the crystal the turn started from'); |
| 539 | check(!!local && !((band(w, 'Safety clause') || {}).text || '').includes('Current crystal.json:'), |
| 540 | 'so the crystal is not filed under the safety clause'); |
| 541 | check(!((band(w, 'Safety clause') || {}).text || '').includes('RANUNCULUS'), |
| 542 | 'nor the user\'s own house rules, which are drawn under their own heading'); |
| 543 | check(((band(w, 'Standing instructions') || {}).text || '').includes('RANUNCULUS'), |
| 544 | 'and they reach a daimon too, on the far side of this turn\'s own paragraph'); |
| 545 | await shot(s, 'daimon-wire'); |
| 546 | |
| 547 | } catch (e) { |
| 548 | check(false, 'the run finished', String(e && e.message || e)); |
| 549 | try { await shot(s, 'threw'); } catch {} |
| 550 | } finally { |
| 551 | await s.close(); |
| 552 | } |
| 553 | |
| 554 | console.log(failures === 0 |
| 555 | ? `\nverify_wire: all checks pass.` |
| 556 | : `\nverify_wire: ${failures} failed.`); |
| 557 | process.exit(failures === 0 ? 0 : 1); |