diff --git a/BUILD-LOG.md b/BUILD-LOG.md index 4cd11068a..8e4184249 100644 --- a/BUILD-LOG.md +++ b/BUILD-LOG.md @@ -5457,3 +5457,9 @@ Row 299 (573, #1806, Gander, Filbert): before, the P10 band test timed a fault r 2026-10-24 | Goldie | Row325 / #1821 before (mos-1563/1564) | Real .mosaic/project.json idstack disagrees with Case1 project's mosaic-stack key; row323 scratch fixture omitted project file and only warned. New goldie/row325 at99930362e changes only that JSON key and expressly authorized old assertion key; new real-byte project-copy validation tests require valid and reject old key. Mutant prepared; no tests before Sage CLEAR, no queue writes/host actions. 2026-10-24 | Goldie | Row325 / #1821 after (sage-3882 CLEAR) | Only Case1 projects key becomesstack; only old test70 expected key updated under mos-1564. New3-case test copies real .mosaic/project.json bytes, requires CLI project valid, rejects old key. Serialized shortTMP timeout-k10: new3/3, business264/264, old-key mutant killed/restored SHA9e43bfef, restored new3/3; all positive0 fail/skip/cancel. Manifest4/4 verifies including unchanged project document. Logs/load lines and mutant diff in BUILD.md; no queue writes/host action, Gladstone review/Sage gate pending. + +2026-10-24 | Goldie | Row265 / #1770 before, recorded during authorized verification | C2-09b has no project room verbs. On goldie/row265 at ec86f2c84, add only room.open/send/read, pure exported leading-tag parser, README verb rows and new bus tests. Sage-3921 fixes EOF-tag and message-only timestamp readings; tester list/helpers/schema stay unchanged. Claim rev3814; Sage-3932 CLEAR for serialized new files, whole bus and twelve refusal mutants. No old-test edits authorized; stop rules apply. + +2026-10-24 | Goldie | Row265 / #1770 first slot STOP (sage-3932) | New parser14/14, rooms18/18 green; serialized bus stops at old project-runtime.test.mjs:14 import-closure pin, partial619 ok/1 not ok. New pure address.mjs import is absent from expected imports. Static trace also needs address.mjs in seat RUNNER_FILES. No old-test/product correction, no mutant/restored run, no green commit/push; old-pin and out-of-owned-files mount change sent to Sage for ruling. Audit prose missed the direct source pin despite raw grep hit; correction recorded in BUILD.md. + +2026-10-24 | Goldie | Row265 / #1770 verified (mos-1567, sage-3942) | Approved correction adds solely address import pin + Row265 comment to project-runtime.test and one pure-source RUNNER_FILES mount; new block checks parser has no imports. Serialized fresh-short-TMP timeout-k10: unchanged sandbox10/10, parser15/15, rooms18/18, full bus957/957, twelve named refusal mutants killed (each Node1, driver0), restored parser15/15 + rooms18/18. All positives zero fail/skip/cancel; every mutation restored full broker SHA71b32ee4. Old first-red evidence retained, no other old-test change. Slot returned goldie-0034; Boyd packet explicitly includes mount-list review, candidate push next. diff --git a/agents/goldie/work/queue-265/BUILD.md b/agents/goldie/work/queue-265/BUILD.md new file mode 100644 index 000000000..1a1b1f503 --- /dev/null +++ b/agents/goldie/work/queue-265/BUILD.md @@ -0,0 +1,134 @@ +# Row 265 / #1770 — project rooms + +Current result: verified under sage-3942; slot returned in goldie-0034. Full bus 957/957, sandbox 10/10, parser 15/15, rooms 18/18, twelve mutants killed, restored files green. Review/push packet below includes mos-1567 closure scope. + +Goldie, 2026-10-24. Branch goldie/row265, base ec86f2c847f6154a0677c6ed23c87dabc2c665d0. Brief docs/plans/2026-10-20_case2-rows.md § C2-09b rooms at blob3a5ed91a4fb3462df67e6939ae6f18290ac9b453; design agents/gosalyn/work/case2/DESIGN.md §6. Sage-3915 transfers ownership from Rocko to Goldie and review from Dewey to Boyd. No old test edit authorized. All tests/mutants wait for Sage CLEAR; queued behind Poe, Bentina and Sage's326 gate. + +## Design and clarified readings + +A room is a bus project. Members are all loaded business instances whose definitions are in THREAD_DEFINITIONS, sorted by key. Canonical room threads have those keys as names and belong to the requesting human. open checks all name/instance/owner collisions before creating anything; it delegates missing inserts to existing thread.create dispatch, including default session metadata. send/read require the complete canonical set. No schema or migration change. + +Sage-3921 settles two brief readings: the single captured timestamp applies to the room.send MESSAGE rows; existing route/delivery/event helpers keep their own timestamps in the same dispatch transaction. They are unchanged. A final EOF-terminated @name is a tag, so bare @continuity is invalid-request rather than a broadcast. Leading whitespace is accepted. Pure parseAddress returns {tags,body}, where body is the unaddressed remainder; room.send stores the ORIGINAL input unchanged. Export it from the bus index and expose a separate reusable test table for C2-10c parity. + +send resolves tags against current #named results and member instance keys, deduplicates recipients, and refuses multiple member matches. It checks each target's current default-session fence and messaging rule, then inserts one normal INFO message, route and human.input per target under the existing request transaction. All message rows share one at. No tag broadcasts; tags after the first non-tag token remain plain text. + +read includes all sessions of the caller's canonical threads, globally ordered by messages.seq. Each entry retains first-message fields plus thread/session/instance and adds ids (every grouped message id) and to (each instance once). Only consecutive human rows with identical at/body/class group, and a repeated instance starts a new entry, so same-millisecond identical sends remain separate. Paging limit counts merged entries (THREAD_PAGE defaults/max); clients continue after the last ids element. Cursor must belong to a room thread. Bounded raw scan limit*memberCount+1 completes requested groups without reading unbounded history. Sage-3921 accepts these routine choices as proposed in goldie-0030. + +## Scope / steps + +1. Read current thread.create/send/messages, transaction wrapper, naming and messaging helpers; audit source readers/pins. Claim265 via queue after next. Done/read, claim recorded at rev 3814 with op goldie-0265-start-1 (same-op replay confirmed). +2. Add parser + index export, only three room cases in broker, README verb rows and new bus tests/fixtures. Do not modify existing thread/messaging/ACL/helper behavior. +3. New tests cover all brief cases, atomicity across message/route/input rows, ownership/projects, permissions, paging/grouping and session fences. No suites or test runs before CLEAR. +4. Record exact refusal→test→mutant matrix here before requesting the slot; one targeted killed mutant per named refusal, restoration byte-checks. New files whole then bus suite under CLEAR, serial timeout-k10, fresh31-byte mktemp TMPDIR plus UTC/load/busy header. +5. Before in-review, grep packages/*/tests for broker.mjs/TESTER_VERBS and changed-file pins. Push own branch and hand tip/base/manifest directly to Sage/Boyd. Sage owns request posting/landing. + +No Console/Discord/CLI room surface or cross-agent reply visibility added. No tester verb, role, schema or secret change. No host action or credential access. + +## New tests and refusal / mutant matrix + +Only new test files and a new reusable vector fixture are added. No pre-existing test changes. Parser table: address.test.mjs consumes address-cases.mjs, including final EOF tags, leading whitespace, tags after the first word, repetition and blank/tag-only remainder. Broker cases: rooms.test.mjs covers sorted/reused canonical threads, other-instance and other-owner collisions, all three verbs' human/tester gates, missing projects/member set, partial/foreign/wrong-instance rooms, key and current-name addressing, ambiguity/same-member name+key, unknown/non-member/old-name tags, tags-only bodies, one/many/broadcast targets, original bytes, shared message timestamp, routes/deliveries/human.input, denied last recipient, clearing last session, all-session merged reads, agent reply metadata, merged paging/default/max, foreign/noncanonical/absent cursors, and same-at repeated sends. Synthetic historical rows append valid rows; they do not rewrite evidence or clocks. The same-at batches repeat the first recipient at each boundary, proving identical sends remain distinct. + +Each named refusal has one mutation of its enforcement, run against the entire new rooms.test.mjs file. The driver mutants.py stores the exact original→mutant diff in logs/Mxx-*.diff, logs UTC/load/busy/TMPDIR, accepts only Node exit 1 with the named target test failing, and restores/verifies full broker bytes and SHA256 in finally. No mutation is retained. Generic pre-existing request/schema/argument policies are exercised through existing code; the matrix names the refusals added or deliberately inherited by these room cases. + +| Mutant | Refusal | Guard relaxed | Required failing new test | +| --- | --- | --- | --- | +| M01-human | human-required | remove common room human check | room verbs require the human | +| M02-tester | tester-refused | add the three room verbs to TESTER_VERBS | room verbs require the human (explicit absence pin) | +| M03-project | project-not-found | remove room project lookup check | room verbs refuse missing projects | +| M04-members | thread-instance-missing | allow empty member set | room verbs refuse missing projects | +| M05-collision | room-thread-name-taken | remove canonical owner/instance collision check | room.open preflights instance collisions | +| M06-open | room-not-open | remove missing canonical thread check | send/read refuse a missing canonical member | +| M07-body | invalid-request | allow empty parsed remainder | unknown or non-member addressees and tags-only bodies | +| M08-unknown | unknown-addressee | allow zero member matches | unknown or non-member addressees and tags-only bodies | +| M09-ambiguous | addressee-ambiguous | allow multiple member matches | a legacy name colliding with another member key | +| M10-messaging | message-sender-refused | omit room target messaging check | messaging denial on the last target | +| M11-cursor | message-not-found | unknown room cursor falls back to seq zero | room.read groups one send, pages | +| M12-fence | session-clearing | omit room target session fence | a clearing last member fences the whole send | + +For M02, the existing human check remains a second barrier: the mutant deliberately relaxes only the tester allowlist; the new explicit absence assertion kills it without loosening or running the old pin. Existing helpers keep their full session-state behavior. No helper or schema mutation ships. + +## Reader / hash audit + +`rg -n 'broker\.mjs|TESTER_VERBS' packages/*/tests --glob '!fixtures/*'` produced reader-grep.txt (133 hits at audit time). These are imports except the concrete source read at bus/tests/limit-checks.test.mjs:323-326 (forbidden limit-check/token-limit imports) and the tester set assertions at bus/tests/tester-writes.test.mjs:18-32. The latter stays byte-for-byte unchanged. bus/tests/viewer.test.mjs:116 builds its coverage set from exported verb lists; rooms do not widen those lists. No old test needs an edit. + +Pin check: recursively inspect packages/*/tests plus scripts/test-*.sh for changed-file mentions (broker.mjs, address.mjs, packages/bus/README.md, packages/bus/src/index.mjs) with literal 64-hex values within eight lines: pin-context-grep.txt records no hits. A direct same-line hash/digest/sha256 grep finds registrySha256 imports only, which hash host registry data rather than broker bytes. The independent channel-proof.test.mjs baselines cover proof/transport files, none changed here. Positive control: pin-positive-control.txt records actual schema.sql / SCHEMA_SHA256 / createHash references in bus/tests/schema.test.mjs and bus/src/store.mjs. Schema bytes are unchanged. + +## Requested serialized slot plan (not run) + +At each run: fresh `TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/XXXXXX)`, date -u, uptime, and `bash /mnt/storage/scratch/sage-busy-now.sh 2>/dev/null | tail -1` in the log. No concurrent test runs. Ignored root node_modules symlink targets the canonical checkout; it will not be committed. + +1. `timeout -k 10 120 node --test packages/bus/tests/address.test.mjs` whole. +2. `timeout -k 10 120 node --test packages/bus/tests/rooms.test.mjs` whole. +3. `timeout -k 10 600 node --test --test-concurrency=1 packages/bus/tests/*.test.mjs` bus suite whole. +4. `python3 agents/goldie/work/queue-265/mutants.py`: M01 through M12 serially, each runs `timeout -k 10 120 node --test packages/bus/tests/rooms.test.mjs` whole, fresh TMPDIR/header; stop on a live mutant or wrong failure. Record each diff and restoration digest. +5. Restored address.test.mjs whole and rooms.test.mjs whole separately, same commands as 1/2 and fresh directories/header each. + +Estimate 5–10 minutes; bus timeout ceiling 10 minutes, individual new/mutant ceilings 2 minutes each. On first new-test red, record/report it. On any old-test red or need for old-test change, stop and ask Sage. Static node --check for new modules/tests and git diff --check passed; no tests or mutants run yet. + +## First slot STOP — sage-3932 + +2026-10-24T04:27Z: stopped on an old-test red. No old test changed or rerun, no mutant executed, no product adjustment after the failure, no commit/push claiming verification. + +| Step | Exit | Counts / disposition | Log | +| --- | --- | --- | --- | +| 1 address whole | 0 | 14 pass, 0 fail/skip/cancel; 58.559 ms | logs/01-address.log | +| 2 rooms whole | 0 | 18 pass, 0 fail/skip/cancel; 1067.524 ms | logs/02-rooms.log | +| 3 bus whole | 1 (stopped) | partial 619 top-level ok, 1 not ok; not a completed-suite result | logs/03-bus.log | +| 4 twelve mutants | not run | stop rule | none | +| 5 restored files | not run | stop rule; no mutations were applied | none | + +All three run headers use UTC/date, uptime, helper CLEAR and distinct 31-byte scratch TMPDIRs. Bus began04:25:22Z at load2.80/2.91/2.61, TMPDIR /mnt/storage/scratch/tmp/LaZAaK. Parser used /mnt/storage/scratch/tmp/rqcTH7; rooms has its own header. Explicit TAP reporter was selected for rooms/bus and the mutant driver so failure detection/counts are stable across Node26's default spec reporter; this changes reporting only. + +The old red is project-runtime.test.mjs:12, assertion:14, test620, “Broker keeps the sandbox client import closure: only the listed imports, no lazy project import”. Actual imports include the new `import { parseAddress } from './address.mjs';`; the pinned array omits it. ERR_ASSERTION deepStrictEqual; individual duration2.469251ms. The full expected/actual/stack is in 03-bus.log. No other red was observed before stopping. + +Reader-audit correction: reader-grep.txt:100 already contains project-runtime.test.mjs:13, a direct broker source read. The earlier prose incorrectly described the import-closure assertion as a normal import; I missed this old source pin during inspection. This is not a hash pin. Follow-up static trace finds seat/src/sandbox.mjs:50-60 RUNNER_FILES includes broker and landing but not address; seat/tests/sandbox.test.mjs:45-53 compares this mount list to the recursively imported source closure. A parser dependency therefore also needs the one new RUNNER_FILES path. Both an old-test pin change and this out-of-owned-files product change require Sage/Mos approval before edits. Recommended bounded change: add only the parser import to the project-runtime expected imports, add only packages/bus/src/address.mjs beside broker in RUNNER_FILES, and leave the generic sandbox closure test unchanged. + +Broker bytes remain SHA256 71b32ee4afa322793aad81231441ee7badc3e5aff65ff008c6cc0f7f63b72781. git diff --check still passes. Slot returned in goldie-0032, pending ruling/new CLEAR; no reruns. + +## mos-1567 approved closure correction / second slot request + +Sage-3940 relays Mos's mos-1567 and widened row scope (sage-3939). Applied exactly the approved pre-existing-file edits: project-runtime.test.mjs adds two lines at17-18, one Row265 comment and the parser import in the pinned array, no other line changed (numstat2 added/0 deleted); seat/src/sandbox.mjs adds only `packages/bus/src/address.mjs` immediately after broker in RUNNER_FILES. seat/tests/sandbox.test.mjs stays byte-for-byte unchanged. The parser remains a separate pure module. A NEW block in address.test.mjs asserts `doesNotMatch(/\bimport\b/)` on its source, with a node:fs import in that new test file; no existing assertion is moved or weakened. + +Review packet must explicitly call out the RUNNER_FILES mount-list entry for Boyd's review. This is a read-only source-file mount needed by the broker's static dependency, with no new module dependency or role/verb authority. The first-run import pin failure remains recorded above and in logs/03-bus.log; new logs will have rerun-prefixed names, preserving first-slot bytes. + +Requested rerun order, awaiting CLEAR after Bentina: first `timeout -k 10 180 node --test --test-reporter=tap packages/seat/tests/sandbox.test.mjs` whole, then the original five steps again from step1: address whole (now15 cases), rooms whole, bus whole with concurrency1 (includes project-runtime whole), M01–M12 whole-room-file mutants, then restored address and rooms files separately. Explicit TAP reporter is used throughout for stable logs/kill matching; timeouts otherwise120 per new/mutant run and600 for bus. Each run still gets fresh short mktemp TMPDIR, date/uptime/busy header, serialized with the same stop rule. Estimate5–10minutes. Only static node --check and git diff --check run since the STOP; all pass. No new test run yet. + +## Verified result — sage-3942 CLEAR, slot returned goldie-0034 + +All rerun positives exit 0 with zero failures, skips or cancellations. Full bus suite passed 957/957, including the approved project-runtime import pin. The unchanged sandbox test passed 10/10, including its recursive mount/import-closure assertion. New parser cases passed 15/15, rooms 18/18, and both passed again after restoration. No tests run after slot return. + +| Run | Exit | Pass / fail | Duration ms | UTC / load / helper | Log | +| --- | --- | --- | --- | --- | --- | +| rerun-00-sandbox | 0 | 10 / 0 | 5526.027699 | Sat Oct 24 04:31:35 UTC 2026; 23:31:35 up 10 days, 14:51, 2 users, load average: 2.83, 2.91, 2.73; CLEAR | [log](logs/rerun-00-sandbox.log.gz) | +| rerun-01-address | 0 | 15 / 0 | 54.634817 | Sat Oct 24 04:31:45 UTC 2026; 23:31:45 up 10 days, 14:51, 2 users, load average: 2.54, 2.84, 2.71; CLEAR | [log](logs/rerun-01-address.log.gz) | +| rerun-02-rooms | 0 | 18 / 0 | 921.788987 | Sat Oct 24 04:31:52 UTC 2026; 23:31:52 up 10 days, 14:51, 2 users, load average: 2.42, 2.81, 2.70; CLEAR | [log](logs/rerun-02-rooms.log.gz) | +| rerun-03-bus | 0 | 957 / 0 | 118397.418425 | Sat Oct 24 04:32:01 UTC 2026; 23:32:01 up 10 days, 14:51, 2 users, load average: 2.28, 2.77, 2.68; CLEAR | [log](logs/rerun-03-bus.log.gz) | +| rerun-05-restored-address | 0 | 15 / 0 | 54.587943 | Sat Oct 24 04:35:02 UTC 2026; 23:35:02 up 10 days, 14:54, 2 users, load average: 3.10, 3.07, 2.83; CLEAR | [log](logs/rerun-05-restored-address.log.gz) | +| rerun-05-restored-rooms | 0 | 18 / 0 | 991.877991 | Sat Oct 24 04:35:11 UTC 2026; 23:35:11 up 10 days, 14:55, 2 users, load average: 3.17, 3.09, 2.84; CLEAR | [log](logs/rerun-05-restored-rooms.log.gz) | + +Mutant driver exit 0; all twelve target tests killed their guard relaxation with Node exit 1. Every mutant log records exact bytes restored to broker SHA256 `71b32ee4afa322793aad81231441ee7badc3e5aff65ff008c6cc0f7f63b72781`. Driver receipt: [rerun-04-mutants-driver.log](logs/rerun-04-mutants-driver.log.gz). + +| Mutant | Node exit | Pass / fail | Exact diff | Raw log | +| --- | --- | --- | --- | --- | +| M01-human | 1, killed | 16 / 2 | [diff](logs/M01-human.diff) | [log](logs/M01-human.log.gz) | +| M02-tester | 1, killed | 17 / 1 | [diff](logs/M02-tester.diff) | [log](logs/M02-tester.log.gz) | +| M03-project | 1, killed | 17 / 1 | [diff](logs/M03-project.diff) | [log](logs/M03-project.log.gz) | +| M04-members | 1, killed | 17 / 1 | [diff](logs/M04-members.diff) | [log](logs/M04-members.log.gz) | +| M05-collision | 1, killed | 14 / 4 | [diff](logs/M05-collision.diff) | [log](logs/M05-collision.log.gz) | +| M06-open | 1, killed | 17 / 1 | [diff](logs/M06-open.diff) | [log](logs/M06-open.log.gz) | +| M07-body | 1, killed | 17 / 1 | [diff](logs/M07-body.diff) | [log](logs/M07-body.log.gz) | +| M08-unknown | 1, killed | 16 / 2 | [diff](logs/M08-unknown.diff) | [log](logs/M08-unknown.log.gz) | +| M09-ambiguous | 1, killed | 17 / 1 | [diff](logs/M09-ambiguous.diff) | [log](logs/M09-ambiguous.log.gz) | +| M10-messaging | 1, killed | 17 / 1 | [diff](logs/M10-messaging.diff) | [log](logs/M10-messaging.log.gz) | +| M11-cursor | 1, killed | 17 / 1 | [diff](logs/M11-cursor.diff) | [log](logs/M11-cursor.log.gz) | +| M12-fence | 1, killed | 17 / 1 | [diff](logs/M12-fence.diff) | [log](logs/M12-fence.log.gz) | + +## Final review scope and packet + +Boyd reviews the three room cases, pure parser/export, README rows, new parser/room tests, and **the added read-only RUNNER_FILES mount for packages/bus/src/address.mjs**. mos-1567 authorizes that one sandbox source line and the sole old-test delta: project-runtime.test.mjs:17-18 adds exactly the Row265 comment and parser import pin (numstat 2 added / 0 deleted). Byte comparison against base after removing those additions proves all other old-test bytes unchanged. Removing the single RUNNER_FILES addition likewise restores sandbox.mjs to base. sandbox.test.mjs has no byte change. New address.test.mjs block proves the mounted parser source has no import word. + +The widened changed-file hash-context audit includes sandbox.mjs: [pin-context-with-sandbox.txt](pin-context-with-sandbox.txt), zero 64-hex-near-reference hits. The original reader and positive-control receipts remain alongside it; the missed source pin and first STOP remain recorded, not concealed. No role/schema/tester allowlist or helper change ships. BUILD-LOG and SESSIONS only append tracking entries. + +Base: `ec86f2c847f6154a0677c6ed23c87dabc2c665d0`. Branch: `goldie/row265`. MANIFEST.sha256 lists every changed source/test/tracking/evidence file except itself, with repository-relative paths, and is verified before commit/push. Raw first-red logs are retained separately from green reruns. + +Evidence packaging: logs are stored losslessly as `.log.gz`; decompress with `gzip -cd`. Decompression was byte-compared with each raw run log before packing. This preserves the original failure/Node whitespace while keeping the committed diff whitespace-clean. The run driver recreates raw `.log` files when used. Mutant busy-check headers identify only their own Python driver PID3686656 as BUSY; positive runs report CLEAR. The driver held the assigned slot throughout, with serialized child tests. diff --git a/agents/goldie/work/queue-265/MANIFEST.sha256 b/agents/goldie/work/queue-265/MANIFEST.sha256 new file mode 100644 index 000000000..f4f022649 --- /dev/null +++ b/agents/goldie/work/queue-265/MANIFEST.sha256 @@ -0,0 +1,51 @@ +996e9d0c1e7a8436691ce5d5d89b4eb940b07aafb3386f63a9847d203f43d998 BUILD-LOG.md +26b5821895a152ddac66a26f16585c6c48ec71d99ca7e7c2b9a4d5a71943c5b4 agents/goldie/work/queue-265/BUILD.md +1289c0f9f4e857590490a0972b5300cc8fb1a86e3ef79fe63e47472709e982f6 agents/goldie/work/queue-265/logs/01-address.log.gz +bc0ac95454ca4a4478641832e68caeb82bfe8b1fdc9abc0b80f1126ef42b7be5 agents/goldie/work/queue-265/logs/02-rooms.log.gz +459a45bd772576847338a438b27f4f578a2a660be744e1404fd239d192c6957c agents/goldie/work/queue-265/logs/03-bus.log.gz +c30cecb149e98d64f285e81a82d07a0a89dad5ea223f9031b543230615663bb5 agents/goldie/work/queue-265/logs/M01-human.diff +a75e02d91ff46fbfaf627beea943183b769e417bcb572bf11f778f0fca7fcf29 agents/goldie/work/queue-265/logs/M01-human.log.gz +b78b1c5b205272411710f56a981acf58dc09c46333369924b98c4c8657dc949c agents/goldie/work/queue-265/logs/M02-tester.diff +6a680dcb1db461cce3a3a2e066aa1245e4f84aa6754354058009bc0f6ebf4185 agents/goldie/work/queue-265/logs/M02-tester.log.gz +02a6cfab404405099fc39eb83413614c67a0c4c3f87cd23c3849a35d1055ddb0 agents/goldie/work/queue-265/logs/M03-project.diff +8b7cd0c568510e49b61fa14fce7b2680025b773eda884e0a84163d240a124e39 agents/goldie/work/queue-265/logs/M03-project.log.gz +18b2cb43f2799301cf934e4c327bc1a03e08ff8d11ff590cb9677c86dfd43ff0 agents/goldie/work/queue-265/logs/M04-members.diff +195f9ec57c9494552566d2f8b87f3b17a210b811f8b556dd65db89814e237816 agents/goldie/work/queue-265/logs/M04-members.log.gz +a773e214c76acf5285ac4ca17cdd90dd552ddc79157abe75ef6de7f67a0b6a70 agents/goldie/work/queue-265/logs/M05-collision.diff +1148cda3f6aaa7f6aeaab2f95c932114ddeac5bfd9318a6b77a4148079d486ed agents/goldie/work/queue-265/logs/M05-collision.log.gz +56400065c9cdb6305763b2bff8c0e06e9ae1223fa89b4467634a9951622d3fbe agents/goldie/work/queue-265/logs/M06-open.diff +b8f9bd9a5c488e604d0afe3feda4a2602359a94a49c7fdbd2f0f6a421b0ca1ce agents/goldie/work/queue-265/logs/M06-open.log.gz +2c1b745a15602cdcaf194131e4d6e0f8fa349e9712ce2e8cb6e695699e4ec19d agents/goldie/work/queue-265/logs/M07-body.diff +4857461e7a2ceb01a6446acb4708a732949a0d8d1a85fa2f6d10bedde6b9e11b agents/goldie/work/queue-265/logs/M07-body.log.gz +e3480e69d016caea6c1fbcdfa022b18214a10e8deca4409d0dc2f72261d2ee92 agents/goldie/work/queue-265/logs/M08-unknown.diff +3d0f347f21df3c388a4f39679d4694aadc9684ae8b8ad2d30bf0bb316e48dcb0 agents/goldie/work/queue-265/logs/M08-unknown.log.gz +62420ed20affcab256facee05414b8b760a4c79a60f60be0c06c88abb61ec6ae agents/goldie/work/queue-265/logs/M09-ambiguous.diff +14a18d61344df592e885c46ace34e0c59914f4fbe591fa99eeb3d81fa8ef8bf1 agents/goldie/work/queue-265/logs/M09-ambiguous.log.gz +0671bf77394f82df92252275171fd9edea2e5b2e7143387840d03bda544676bf agents/goldie/work/queue-265/logs/M10-messaging.diff +4202215d40999e0d1cc98ebfb5445f0c8fd4688e6ca968e85d225723076ca8a7 agents/goldie/work/queue-265/logs/M10-messaging.log.gz +042598ecb551e761d37af898f69e17ec137d2f39687dbf81ce5e1eeb98a38202 agents/goldie/work/queue-265/logs/M11-cursor.diff +deff3d0514c379bd3d9c6f6bbc60db59b9dce2baaafd6fa611711c98cc8365ce agents/goldie/work/queue-265/logs/M11-cursor.log.gz +780e70fbcb2328006c8d9ace75aa62fcd0d935b4d758b50b0f5c5d5c55604122 agents/goldie/work/queue-265/logs/M12-fence.diff +b629b41a39c028b70d4b847c351d248d6d70e8175d6d27f563b0ed8817f89642 agents/goldie/work/queue-265/logs/M12-fence.log.gz +1b8c3cec31b44ff018a1bc74fef95996bcbdd21d340e2d2605154ba645648040 agents/goldie/work/queue-265/logs/rerun-00-sandbox.log.gz +d18c5a9432de06ebec36c937da7dfb1a3e79dc397b33ff624058af351eee24dd agents/goldie/work/queue-265/logs/rerun-01-address.log.gz +d10b83ccec56e47d8c7c74d8304fc4fc87b8ea92a50098380cd02b8012c32d09 agents/goldie/work/queue-265/logs/rerun-02-rooms.log.gz +6ff5955fe5c85f51c1cf4daeb8eddd61ef1a30e170221f99bfb08da8304d7e4a agents/goldie/work/queue-265/logs/rerun-03-bus.log.gz +eeba0182f06cee0c06cd15ecf7ef43aaeffd7149b2202cc0653078c20bb46198 agents/goldie/work/queue-265/logs/rerun-04-mutants-driver.log.gz +5fe9fe35af60ef951c5c68b040aa9bb6067d9c746e143bb5782eb428acd4e4fe agents/goldie/work/queue-265/logs/rerun-05-restored-address.log.gz +2b3f3305ff5f6926a0269bd6c88d5f2e4a89727eefcf99625bebb51a70abb649 agents/goldie/work/queue-265/logs/rerun-05-restored-rooms.log.gz +f11ae40d923affad1936fe51d05981ac201a1051bc9f877d75fc1d544c6e38f1 agents/goldie/work/queue-265/mutants.py +e9cdf16670a16a14f3dd722a69c63a1785e4deb3608ee05d519b0ec8c1a9ca0b agents/goldie/work/queue-265/pin-context-grep.txt +491ba3bb25e918049249178f3c6a67b2b4f56a197f0a67059acb2420cd03b73c agents/goldie/work/queue-265/pin-context-with-sandbox.txt +784fb42bff187c0eebcf7756942ec3cf56f8faa7b54ce4ad96181b81fe46f0e9 agents/goldie/work/queue-265/pin-positive-control.txt +766ead519eefbde958bbdbf77bf858b10dd1e3d9dee8ab5d356fe821ce7602fe agents/goldie/work/queue-265/reader-grep.txt +6c3c44eb943f33d5bb547f542c77da4d6f04e738056466bc491c2f6fd5f67946 docs/SESSIONS.md +1ba33af1f42c88a5346d25fe915a44ee70a27fbc6b9ab880db0866cbdad5d45d packages/bus/README.md +232a1c77c2f17011c53b55fc6052f70985598210769d41d7c2c0b535328aad23 packages/bus/src/address.mjs +71b32ee4afa322793aad81231441ee7badc3e5aff65ff008c6cc0f7f63b72781 packages/bus/src/broker.mjs +ed57d48d2af4a225259e0a44c86a30b12792ec9f725dd9c89251569b7dc89e5a packages/bus/src/index.mjs +a6725ffe30d44981a9b9d3a3c319f7e96a46ec7bd74f0c8d869d9a798eca1a81 packages/bus/tests/address-cases.mjs +fae431bc3743b224f69da33c4cebe376b47257d67f52d25d3ab0cb59f224a607 packages/bus/tests/address.test.mjs +8594e94384c1c8b7234cfa8bbc17456cff86d9adef936cd1059a0d5a60e4546b packages/bus/tests/project-runtime.test.mjs +892c3709393fa8962710bc07565d1097bbd79ea1fbfda34af5f9385db34ed0fa packages/bus/tests/rooms.test.mjs +83019fa6b39ad9fd79b84627856b4dec82683d8261b8cba9b9ac840e0c489268 packages/seat/src/sandbox.mjs diff --git a/agents/goldie/work/queue-265/logs/01-address.log.gz b/agents/goldie/work/queue-265/logs/01-address.log.gz new file mode 100644 index 000000000..df11d7b41 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/01-address.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/02-rooms.log.gz b/agents/goldie/work/queue-265/logs/02-rooms.log.gz new file mode 100644 index 000000000..f2c4752a2 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/02-rooms.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/03-bus.log.gz b/agents/goldie/work/queue-265/logs/03-bus.log.gz new file mode 100644 index 000000000..8a2409145 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/03-bus.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/M01-human.diff b/agents/goldie/work/queue-265/logs/M01-human.diff new file mode 100644 index 000000000..851cfd33c --- /dev/null +++ b/agents/goldie/work/queue-265/logs/M01-human.diff @@ -0,0 +1,10 @@ +--- broker.original ++++ M01-human +@@ -6423,7 +6423,6 @@ + case 'room.open': + case 'room.send': + case 'room.read': { +- this.#human(s); + const opening = verb === 'room.open'; + const sending = verb === 'room.send'; + keys(a, opening ? ['project'] : sending ? ['project', 'body'] : ['project', 'after', 'limit'], diff --git a/agents/goldie/work/queue-265/logs/M01-human.log.gz b/agents/goldie/work/queue-265/logs/M01-human.log.gz new file mode 100644 index 000000000..7be6068a3 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/M01-human.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/M02-tester.diff b/agents/goldie/work/queue-265/logs/M02-tester.diff new file mode 100644 index 000000000..5a0736b9b --- /dev/null +++ b/agents/goldie/work/queue-265/logs/M02-tester.diff @@ -0,0 +1,12 @@ +--- broker.original ++++ M02-tester +@@ -122,7 +122,8 @@ + // Row 175 (#1673, Q173): the test system's reads and two writes, name.set and project.create, + // each recorded with `tester` as its actor. The organization branch keeps its own list below. + // Row 177 (#1675, Q173): the tester's own threads, owner `tester`, and a stop of their runs. +-export const TESTER_VERBS = Object.freeze(['message.send', 'message.receive', 'message.read', 'inbox', 'agents', 'tasks', 'trail', 'decisions', 'messages', 'replies', ++export const TESTER_VERBS = Object.freeze([ ++ 'room.open', 'room.send', 'room.read','message.send', 'message.receive', 'message.read', 'inbox', 'agents', 'tasks', 'trail', 'decisions', 'messages', 'replies', + 'usage', 'names', 'limits', 'plugins', 'instances', 'project.list', 'thread.list', 'name.set', 'project.create', 'credentials', + 'thread.create', 'thread.open', 'thread.send', 'thread.messages', 'assistant.stop', + // Row 194 (#1692, D4.1): launch and terminate, and the read of the requests they make. diff --git a/agents/goldie/work/queue-265/logs/M02-tester.log.gz b/agents/goldie/work/queue-265/logs/M02-tester.log.gz new file mode 100644 index 000000000..22fd17bab Binary files /dev/null and b/agents/goldie/work/queue-265/logs/M02-tester.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/M03-project.diff b/agents/goldie/work/queue-265/logs/M03-project.diff new file mode 100644 index 000000000..815a04992 --- /dev/null +++ b/agents/goldie/work/queue-265/logs/M03-project.diff @@ -0,0 +1,10 @@ +--- broker.original ++++ M03-project +@@ -6429,7 +6429,6 @@ + keys(a, opening ? ['project'] : sending ? ['project', 'body'] : ['project', 'after', 'limit'], + sending ? ['project', 'body'] : ['project']); + const project = identifier(a.project); +- if (!this.#store.get('SELECT 1 FROM projects WHERE id=? AND business=?', project, s.business)) fail('project-not-found'); + const members = Object.keys(b.roles).filter((instance) => this.#threaded(s.business, instance)).sort(); + if (!members.length) fail('thread-instance-missing'); + const owner = this.#owner(s); diff --git a/agents/goldie/work/queue-265/logs/M03-project.log.gz b/agents/goldie/work/queue-265/logs/M03-project.log.gz new file mode 100644 index 000000000..e4fdb31c1 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/M03-project.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/M04-members.diff b/agents/goldie/work/queue-265/logs/M04-members.diff new file mode 100644 index 000000000..49b81344a --- /dev/null +++ b/agents/goldie/work/queue-265/logs/M04-members.diff @@ -0,0 +1,10 @@ +--- broker.original ++++ M04-members +@@ -6431,7 +6431,6 @@ + const project = identifier(a.project); + if (!this.#store.get('SELECT 1 FROM projects WHERE id=? AND business=?', project, s.business)) fail('project-not-found'); + const members = Object.keys(b.roles).filter((instance) => this.#threaded(s.business, instance)).sort(); +- if (!members.length) fail('thread-instance-missing'); + const owner = this.#owner(s); + // Preflight all collisions/missing threads before open creates or send writes anything. + const threads = members.map((instance) => { diff --git a/agents/goldie/work/queue-265/logs/M04-members.log.gz b/agents/goldie/work/queue-265/logs/M04-members.log.gz new file mode 100644 index 000000000..268dd9f5f Binary files /dev/null and b/agents/goldie/work/queue-265/logs/M04-members.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/M05-collision.diff b/agents/goldie/work/queue-265/logs/M05-collision.diff new file mode 100644 index 000000000..4de439e9b --- /dev/null +++ b/agents/goldie/work/queue-265/logs/M05-collision.diff @@ -0,0 +1,11 @@ +--- broker.original ++++ M05-collision +@@ -6436,8 +6436,6 @@ + // Preflight all collisions/missing threads before open creates or send writes anything. + const threads = members.map((instance) => { + const t = this.#store.get('SELECT * FROM threads WHERE project=? AND name=?', project, instance); +- if (t && (t.instance !== instance || t.owner !== owner)) +- fail(opening ? 'room-thread-name-taken' : 'room-not-open'); + if (!t && !opening) fail('room-not-open'); + return t ?? { instance, name: instance }; + }); diff --git a/agents/goldie/work/queue-265/logs/M05-collision.log.gz b/agents/goldie/work/queue-265/logs/M05-collision.log.gz new file mode 100644 index 000000000..ead833b92 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/M05-collision.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/M06-open.diff b/agents/goldie/work/queue-265/logs/M06-open.diff new file mode 100644 index 000000000..27bc2b924 --- /dev/null +++ b/agents/goldie/work/queue-265/logs/M06-open.diff @@ -0,0 +1,10 @@ +--- broker.original ++++ M06-open +@@ -6438,7 +6438,6 @@ + const t = this.#store.get('SELECT * FROM threads WHERE project=? AND name=?', project, instance); + if (t && (t.instance !== instance || t.owner !== owner)) + fail(opening ? 'room-thread-name-taken' : 'room-not-open'); +- if (!t && !opening) fail('room-not-open'); + return t ?? { instance, name: instance }; + }); + if (opening) { diff --git a/agents/goldie/work/queue-265/logs/M06-open.log.gz b/agents/goldie/work/queue-265/logs/M06-open.log.gz new file mode 100644 index 000000000..48d43143c Binary files /dev/null and b/agents/goldie/work/queue-265/logs/M06-open.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/M07-body.diff b/agents/goldie/work/queue-265/logs/M07-body.diff new file mode 100644 index 000000000..812bfed30 --- /dev/null +++ b/agents/goldie/work/queue-265/logs/M07-body.diff @@ -0,0 +1,10 @@ +--- broker.original ++++ M07-body +@@ -6450,7 +6450,6 @@ + if (sending) { + string(a.body, 32768); + const addressed = parseAddress(a.body); +- if (!addressed.body.trim()) fail('invalid-request'); + const selected = new Set(); + for (const tag of addressed.tags) { + const candidates = new Set([this.#named(s.business, tag), tag].filter((instance) => members.includes(instance))); diff --git a/agents/goldie/work/queue-265/logs/M07-body.log.gz b/agents/goldie/work/queue-265/logs/M07-body.log.gz new file mode 100644 index 000000000..58bfacbe0 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/M07-body.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/M08-unknown.diff b/agents/goldie/work/queue-265/logs/M08-unknown.diff new file mode 100644 index 000000000..d4994939a --- /dev/null +++ b/agents/goldie/work/queue-265/logs/M08-unknown.diff @@ -0,0 +1,10 @@ +--- broker.original ++++ M08-unknown +@@ -6454,7 +6454,6 @@ + const selected = new Set(); + for (const tag of addressed.tags) { + const candidates = new Set([this.#named(s.business, tag), tag].filter((instance) => members.includes(instance))); +- if (!candidates.size) fail('unknown-addressee'); + if (candidates.size > 1) fail('addressee-ambiguous'); + selected.add([...candidates][0]); + } diff --git a/agents/goldie/work/queue-265/logs/M08-unknown.log.gz b/agents/goldie/work/queue-265/logs/M08-unknown.log.gz new file mode 100644 index 000000000..fd42ba9f9 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/M08-unknown.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/M09-ambiguous.diff b/agents/goldie/work/queue-265/logs/M09-ambiguous.diff new file mode 100644 index 000000000..06620fcd5 --- /dev/null +++ b/agents/goldie/work/queue-265/logs/M09-ambiguous.diff @@ -0,0 +1,10 @@ +--- broker.original ++++ M09-ambiguous +@@ -6455,7 +6455,6 @@ + for (const tag of addressed.tags) { + const candidates = new Set([this.#named(s.business, tag), tag].filter((instance) => members.includes(instance))); + if (!candidates.size) fail('unknown-addressee'); +- if (candidates.size > 1) fail('addressee-ambiguous'); + selected.add([...candidates][0]); + } + const targets = threads.filter((t) => !addressed.tags.length || selected.has(t.instance)); diff --git a/agents/goldie/work/queue-265/logs/M09-ambiguous.log.gz b/agents/goldie/work/queue-265/logs/M09-ambiguous.log.gz new file mode 100644 index 000000000..854c93da1 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/M09-ambiguous.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/M10-messaging.diff b/agents/goldie/work/queue-265/logs/M10-messaging.diff new file mode 100644 index 000000000..58f895e38 --- /dev/null +++ b/agents/goldie/work/queue-265/logs/M10-messaging.diff @@ -0,0 +1,10 @@ +--- broker.original ++++ M10-messaging +@@ -6462,7 +6462,6 @@ + const sessions = targets.map((t) => { + const session = this.#defaultSession(t.id); + this.#sessionFence(session); +- this.#checkMessaging(s.business, 'human', t.instance); + return session; + }); + const at = this.#now(); diff --git a/agents/goldie/work/queue-265/logs/M10-messaging.log.gz b/agents/goldie/work/queue-265/logs/M10-messaging.log.gz new file mode 100644 index 000000000..e129b0741 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/M10-messaging.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/M11-cursor.diff b/agents/goldie/work/queue-265/logs/M11-cursor.diff new file mode 100644 index 000000000..82538a6c7 --- /dev/null +++ b/agents/goldie/work/queue-265/logs/M11-cursor.diff @@ -0,0 +1,11 @@ +--- broker.original ++++ M11-cursor +@@ -6488,7 +6488,7 @@ + `SELECT m.seq FROM messages m JOIN message_threads mt ON mt.message=m.id WHERE m.id=? AND m.business=? AND mt.thread IN (${placeholders})`, + identifier(a.after), s.business, ...tids, + )?.seq; +- if (after === undefined) fail('message-not-found'); ++ if (after === undefined) after = 0; + } + // At most one row per instance joins a group; one lookahead row completes the last page. + const rows = this.#store.all( diff --git a/agents/goldie/work/queue-265/logs/M11-cursor.log.gz b/agents/goldie/work/queue-265/logs/M11-cursor.log.gz new file mode 100644 index 000000000..c3fa2589a Binary files /dev/null and b/agents/goldie/work/queue-265/logs/M11-cursor.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/M12-fence.diff b/agents/goldie/work/queue-265/logs/M12-fence.diff new file mode 100644 index 000000000..8a371fe14 --- /dev/null +++ b/agents/goldie/work/queue-265/logs/M12-fence.diff @@ -0,0 +1,10 @@ +--- broker.original ++++ M12-fence +@@ -6461,7 +6461,6 @@ + const targets = threads.filter((t) => !addressed.tags.length || selected.has(t.instance)); + const sessions = targets.map((t) => { + const session = this.#defaultSession(t.id); +- this.#sessionFence(session); + this.#checkMessaging(s.business, 'human', t.instance); + return session; + }); diff --git a/agents/goldie/work/queue-265/logs/M12-fence.log.gz b/agents/goldie/work/queue-265/logs/M12-fence.log.gz new file mode 100644 index 000000000..d73d7a94e Binary files /dev/null and b/agents/goldie/work/queue-265/logs/M12-fence.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/rerun-00-sandbox.log.gz b/agents/goldie/work/queue-265/logs/rerun-00-sandbox.log.gz new file mode 100644 index 000000000..cd28ae52d Binary files /dev/null and b/agents/goldie/work/queue-265/logs/rerun-00-sandbox.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/rerun-01-address.log.gz b/agents/goldie/work/queue-265/logs/rerun-01-address.log.gz new file mode 100644 index 000000000..59a8a71bf Binary files /dev/null and b/agents/goldie/work/queue-265/logs/rerun-01-address.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/rerun-02-rooms.log.gz b/agents/goldie/work/queue-265/logs/rerun-02-rooms.log.gz new file mode 100644 index 000000000..dc4e39613 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/rerun-02-rooms.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/rerun-03-bus.log.gz b/agents/goldie/work/queue-265/logs/rerun-03-bus.log.gz new file mode 100644 index 000000000..b0cf204b3 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/rerun-03-bus.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/rerun-04-mutants-driver.log.gz b/agents/goldie/work/queue-265/logs/rerun-04-mutants-driver.log.gz new file mode 100644 index 000000000..c9689a2f8 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/rerun-04-mutants-driver.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/rerun-05-restored-address.log.gz b/agents/goldie/work/queue-265/logs/rerun-05-restored-address.log.gz new file mode 100644 index 000000000..25bcef2c8 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/rerun-05-restored-address.log.gz differ diff --git a/agents/goldie/work/queue-265/logs/rerun-05-restored-rooms.log.gz b/agents/goldie/work/queue-265/logs/rerun-05-restored-rooms.log.gz new file mode 100644 index 000000000..ae12d5cc0 Binary files /dev/null and b/agents/goldie/work/queue-265/logs/rerun-05-restored-rooms.log.gz differ diff --git a/agents/goldie/work/queue-265/mutants.py b/agents/goldie/work/queue-265/mutants.py new file mode 100644 index 000000000..6cc7f956e --- /dev/null +++ b/agents/goldie/work/queue-265/mutants.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Slot-only runner. Every mutation restores and verifies the complete broker bytes.""" +from pathlib import Path +import hashlib, os, re, subprocess, sys + +ROOT = Path(__file__).resolve().parents[4] +SOURCE = ROOT / 'packages/bus/src/broker.mjs' +OUT = Path(__file__).resolve().parent / 'logs' +MUTANTS = [ + ('M01-human', 'human-required', 'room verbs require the human', ' this.#human(s);\n', ''), + ('M02-tester', 'tester-refused', 'room verbs require the human', None, None), + ('M03-project', 'project-not-found', 'room verbs refuse missing projects', " if (!this.#store.get('SELECT 1 FROM projects WHERE id=? AND business=?', project, s.business)) fail('project-not-found');\n", ''), + ('M04-members', 'thread-instance-missing', 'room verbs refuse missing projects', " if (!members.length) fail('thread-instance-missing');\n", ''), + ('M05-collision', 'room-thread-name-taken', 'room.open preflights instance collisions', " if (t && (t.instance !== instance || t.owner !== owner))\n fail(opening ? 'room-thread-name-taken' : 'room-not-open');\n", ''), + ('M06-open', 'room-not-open', 'send/read refuse a missing canonical member', " if (!t && !opening) fail('room-not-open');\n", ''), + ('M07-body', 'invalid-request', 'unknown or non-member addressees and tags-only bodies', " if (!addressed.body.trim()) fail('invalid-request');\n", ''), + ('M08-unknown', 'unknown-addressee', 'unknown or non-member addressees and tags-only bodies', " if (!candidates.size) fail('unknown-addressee');\n", ''), + ('M09-ambiguous', 'addressee-ambiguous', 'a legacy name colliding with another member key', " if (candidates.size > 1) fail('addressee-ambiguous');\n", ''), + ('M10-messaging', 'message-sender-refused', 'messaging denial on the last target', " this.#checkMessaging(s.business, 'human', t.instance);\n", ''), + ('M11-cursor', 'message-not-found', 'room.read groups one send, pages', " if (after === undefined) fail('message-not-found');\n", ' if (after === undefined) after = 0;\n'), + ('M12-fence', 'session-clearing', 'a clearing last member fences the whole send', ' this.#sessionFence(session);\n', ''), +] + +def sha(data): return hashlib.sha256(data).hexdigest() + +def main(): + OUT.mkdir(exist_ok=True) + original = SOURCE.read_bytes() + text = original.decode() + start = text.index(" case 'room.open':") + end = text.index(" // A thread belongs", start) + block = text[start:end] + for name, refusal, expected, old, new in MUTANTS: + if old is None: + needle = 'export const TESTER_VERBS = Object.freeze([' + assert text.count(needle) == 1 + changed = text.replace(needle, needle + "\n 'room.open', 'room.send', 'room.read',", 1) + else: + assert block.count(old) == 1, (name, block.count(old)) + changed = text[:start] + block.replace(old, new, 1) + text[end:] + # Save conventional original→mutant diff without relying on shell interpolation. + import difflib + (OUT / f'{name}.diff').write_text(''.join(difflib.unified_diff(text.splitlines(True), changed.splitlines(True), fromfile='broker.original', tofile=name))) + tmp = subprocess.check_output(['mktemp', '-d', '/mnt/storage/scratch/tmp/XXXXXX'], text=True).strip() + env = {**os.environ, 'TMPDIR': tmp} + log = OUT / f'{name}.log' + try: + SOURCE.write_text(changed) + with log.open('w') as stream: + for command in [['date', '-u'], ['uptime'], ['bash', '-c', 'bash /mnt/storage/scratch/sage-busy-now.sh 2>/dev/null | tail -1']]: + subprocess.run(command, cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT, check=True) + stream.write(f'TMPDIR={tmp}\nmutant={name} refusal={refusal} original_sha256={sha(original)}\n') + stream.flush() + result = subprocess.run(['timeout', '-k', '10', '120', 'node', '--test', '--test-reporter=tap', 'packages/bus/tests/rooms.test.mjs'], + cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT) + output = log.read_text() + killed = result.returncode == 1 and re.search(r'(?m)^not ok \d+ - ' + re.escape(expected), output) + print(f'{name}: exit={result.returncode} target_killed={bool(killed)} log={log}', flush=True) + if not killed: return 1 + finally: + SOURCE.write_bytes(original) + assert SOURCE.read_bytes() == original + assert sha(SOURCE.read_bytes()) == sha(original) + with log.open('a') as stream: stream.write(f'RESTORED sha256={sha(original)}\n') + return 0 + +if __name__ == '__main__': sys.exit(main()) diff --git a/agents/goldie/work/queue-265/pin-context-grep.txt b/agents/goldie/work/queue-265/pin-context-grep.txt new file mode 100644 index 000000000..2b5e06287 --- /dev/null +++ b/agents/goldie/work/queue-265/pin-context-grep.txt @@ -0,0 +1 @@ +No changed-file mention has a literal 64-hex digest within eight lines. diff --git a/agents/goldie/work/queue-265/pin-context-with-sandbox.txt b/agents/goldie/work/queue-265/pin-context-with-sandbox.txt new file mode 100644 index 000000000..163dbc9ee --- /dev/null +++ b/agents/goldie/work/queue-265/pin-context-with-sandbox.txt @@ -0,0 +1 @@ +No changed-product-file mention has a literal 64-hex digest within eight lines. diff --git a/agents/goldie/work/queue-265/pin-positive-control.txt b/agents/goldie/work/queue-265/pin-positive-control.txt new file mode 100644 index 000000000..ca74f0d18 --- /dev/null +++ b/agents/goldie/work/queue-265/pin-positive-control.txt @@ -0,0 +1,6 @@ +packages/bus/src/store.mjs:15:const ddl = readFileSync(new URL('../schema.sql', import.meta.url), 'utf8'); +packages/bus/src/store.mjs:16:export const SCHEMA_SHA256 = 'a2ba3a46f12e56139ecae9e55227d00847842aedec50c1d92aaae28844ca48e7'; +packages/bus/src/store.mjs:106:const hex = (value) => createHash('sha256').update(value).digest('hex'); +packages/bus/src/store.mjs:107:if (hex(ddl) !== SCHEMA_SHA256) throw Error('schema-source-mismatch'); +packages/bus/tests/schema.test.mjs:17: db.exec(readFileSync(new URL('../schema.sql', import.meta.url), 'utf8')); +packages/bus/tests/schema.test.mjs:90: const hex = (s) => createHash('sha256').update(s).digest('hex'); diff --git a/agents/goldie/work/queue-265/reader-grep.txt b/agents/goldie/work/queue-265/reader-grep.txt new file mode 100644 index 000000000..31f481627 --- /dev/null +++ b/agents/goldie/work/queue-265/reader-grep.txt @@ -0,0 +1,133 @@ +packages/bus/tests/session-compactions.test.mjs:9:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/console-decide.test.mjs:14:import { Broker, CONSOLE_VERBS } from '../src/broker.mjs'; +packages/harness/tests/runner.test.mjs:14:import { Broker, BusError } from "../../bus/src/broker.mjs"; +packages/bus/tests/project-files.test.mjs:8:import { PROJECT_FILE_VERBS } from '../src/broker.mjs'; +packages/cli/tests/lifecycle.test.mjs:19:import { BusError } from "../../bus/src/broker.mjs"; +packages/bus/tests/lifecycle-verbs.test.mjs:11:import { Broker, LIFECYCLE_REQUEST_MS, TESTER_VERBS } from '../src/broker.mjs'; +packages/bus/tests/lifecycle-verbs.test.mjs:74: for (const verb of ['instance.launch', 'instance.end', 'instance.request']) assert.ok(TESTER_VERBS.includes(verb), verb); +packages/bus/tests/lifecycle-verbs.test.mjs:75: assert.equal(TESTER_VERBS.includes('instance.regenerate'), false); +packages/bus/tests/tasks.test.mjs:9:import { Broker, BusError, TASK_VERBS } from '../src/broker.mjs'; +packages/harness/tests/tools.test.mjs:4:import { ORGANIZATION_KEYS as BROKER_ORGANIZATION_KEYS, TASK_PAGE, TASK_PAGE_MAX } from "../../bus/src/broker.mjs"; +packages/seat/tests/session.test.mjs:11:import { Broker } from "../../bus/src/broker.mjs"; +packages/bus/tests/decision-route.test.mjs:12:import { Broker } from '../src/broker.mjs'; +packages/cli/tests/thread-launch.test.mjs:19:import { BusError } from "../../bus/src/broker.mjs"; +packages/seat/tests/sandbox.test.mjs:15:import { Broker } from "../../bus/src/broker.mjs"; +packages/bus/tests/host-run.test.mjs:16:import { Broker, ORGANIZATION_KEYS } from '../src/broker.mjs'; +packages/bus/tests/host-run.test.mjs:181: // organization.set reaches the writer as limits.set (broker.mjs:1379). +packages/cli/tests/helpers.mjs:10:import { BusError, Broker } from "../../bus/src/broker.mjs"; +packages/tasks/tests/fields.test.mjs:10:import { TASK_VERBS } from '../../bus/src/broker.mjs'; +packages/tasks/tests/fields.test.mjs:226: // decision back to that role (broker.mjs, decision.raise). +packages/webui/tests/instances.test.mjs:15:import { BusError } from '../../bus/src/broker.mjs'; +packages/cli/tests/tester-threads.test.mjs:14:import { BusError, Broker } from "../../bus/src/broker.mjs"; +packages/bus/tests/staff-request.test.mjs:8:import { Broker } from '../src/broker.mjs'; +packages/tasks/tests/world.mjs:5:import { Broker } from '../../bus/src/broker.mjs'; +packages/cli/tests/processing-cli.test.mjs:7:import { Broker } from '../../bus/src/broker.mjs'; +packages/bus/tests/grant-host.test.mjs:10:import { Broker, registrySha256 } from '../src/broker.mjs'; +packages/business/tests/workflow-roles.test.mjs:115: // Q169 (broker.mjs END_PROTECTED, #hardWait): revoking the assistant is a hard wait as well. +packages/business/tests/workflow-roles.test.mjs:136: // sage-0709: only recordLand spends a decision (broker.mjs #spent). checkLand refuses decision-consumed after +packages/cli/tests/thread-revoke.test.mjs:18:import { BusError } from "../../bus/src/broker.mjs"; +packages/bus/tests/tester-writes.test.mjs:10:import { Broker, TESTER_VERBS, TESTER_ORGANIZATION_VERBS } from '../src/broker.mjs'; +packages/bus/tests/tester-writes.test.mjs:18:test('row 175: TESTER_VERBS and the organization branch tester set are pinned', () => { +packages/bus/tests/tester-writes.test.mjs:19: assert.deepEqual([...TESTER_VERBS], [ +packages/bus/tests/tester-writes.test.mjs:32: assert.ok(Object.isFrozen(TESTER_VERBS) && Object.isFrozen(TESTER_ORGANIZATION_VERBS)); +packages/cli/tests/stack-release.test.mjs:1993: for(const [f,text] of [['packages/cli/src/cli.mjs',id===A?'// fixture host from before row 163\nprocess.exit(1);\n':script(rHost)],['packages/webui/src/cli.mjs',script(rConsole)],['packages/bus/src/store.mjs',rStore(v,froms)],['packages/bus/src/migrate-schema.mjs',script(rMigrate)],['packages/bus/src/broker.mjs','export class BusError extends Error { constructor(code) { super(code); this.code = code; } }\n']]){mkdirSync(dirname(join(d,f)),{recursive:true});writeFileSync(join(d,f),text)} +packages/cli/tests/session-cli.test.mjs:94: const { Broker } = await import('../../bus/src/broker.mjs'); +packages/cli/tests/session-launch.test.mjs:21:import { BusError } from "../../bus/src/broker.mjs"; +packages/cli/tests/revoke-window.test.mjs:19:import { BusError } from "../../bus/src/broker.mjs"; +packages/bus/tests/chief-of-staff.test.mjs:10:import { Broker, ACTIONS } from '../src/broker.mjs'; +packages/cli/tests/instances.test.mjs:13:import { BusError } from "../../bus/src/broker.mjs"; +packages/cli/tests/thread-cli.test.mjs:14:import { BusError, Broker } from "../../bus/src/broker.mjs"; +packages/bus/tests/processing.test.mjs:6:import { Broker } from '../src/broker.mjs'; +packages/cli/tests/idle-stop-race.test.mjs:17:import { BusError } from "../../bus/src/broker.mjs"; +packages/cli/tests/tester-reads.test.mjs:12:import { BusError } from "../../bus/src/broker.mjs"; +packages/cli/tests/session-clear.test.mjs:18:import { BusError } from "../../bus/src/broker.mjs"; +packages/bus/tests/names.test.mjs:8:import { Broker } from '../src/broker.mjs'; +packages/cli/tests/compactions-launch.test.mjs:24:import { BusError } from "../../bus/src/broker.mjs"; +packages/cli/tests/tester.test.mjs:7:import { BusError, Broker } from "../../bus/src/broker.mjs"; +packages/bus/tests/reload.test.mjs:9:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/agents.test.mjs:9:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/end-launch.test.mjs:9:import { Broker } from '../src/broker.mjs'; +packages/webui/tests/decide-browser.test.mjs:14:import { Broker } from '../../bus/src/broker.mjs'; +packages/bus/tests/credential-verbs.test.mjs:11:import { Broker, CREDENTIAL_VERBS } from '../src/broker.mjs'; +packages/webui/tests/send-browser.test.mjs:13:import { Broker } from '../../bus/src/broker.mjs'; +packages/bus/tests/launch-notice.test.mjs:8:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/messages.test.mjs:3:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/workflow-roles.test.mjs:11:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/library-sync.test.mjs:11:import { Broker, LIBRARY_VERBS } from '../src/broker.mjs'; +packages/bus/tests/credential-status.test.mjs:12:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/gitea-grant-docker.test.mjs:15:import { Broker, GRANT_EVIDENCE } from '../src/broker.mjs'; +packages/bus/tests/thread-migration.test.mjs:11:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/comment-cursors.test.mjs:7:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/thread-claims.test.mjs:11:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/state.test.mjs:7:import { Broker, TASK_PAGE } from '../src/broker.mjs'; +packages/bus/tests/access-revoke.test.mjs:14:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/plugins-verb.test.mjs:9:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/transport.test.mjs:8:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/publish.test.mjs:14:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/publish.test.mjs:221: const { ACTIONS, HARD_WAITS } = await import('../src/broker.mjs'); +packages/bus/tests/tester-threads.test.mjs:11:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/tester-threads.test.mjs:284: // The row #specNotice (broker.mjs) writes for a human save of the spec in project stack. The save +packages/bus/tests/approval-binding.test.mjs:7:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/fire-revokes-grants.test.mjs:15:import { Broker, GRANT_EVIDENCE, registrySha256 } from '../src/broker.mjs'; +packages/bus/tests/daily-quota-runtime.test.mjs:10:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/limit-checks.test.mjs:323: for (const file of ['../src/runtime.mjs', '../src/broker.mjs', '../src/process.mjs']) { +packages/bus/tests/spec-fixtures.mjs:6:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/decisions.test.mjs:9:import { Broker, RECENT } from '../src/broker.mjs'; +packages/bus/tests/peer-transport.test.mjs:9:import { BusError } from '../src/broker.mjs'; +packages/bus/tests/peer-transport.test.mjs:47: const {TASK_VERBS,BusError}=await import('../src/broker.mjs'); +packages/bus/tests/messaging.test.mjs:8:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/rooms.test.mjs:8:import { Broker, THREAD_PAGE, THREAD_PAGE_MAX, TESTER_VERBS } from '../src/broker.mjs'; +packages/bus/tests/rooms.test.mjs:80: assert.ok(!TESTER_VERBS.includes(verb)); +packages/bus/tests/web.test.mjs:8:import { Broker, BusError, WEB_UNTRUSTED } from '../src/broker.mjs'; +packages/bus/tests/replies.test.mjs:11:import { Broker, REPLIES } from '../src/broker.mjs'; +packages/bus/tests/thread-shares.test.mjs:14:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/message-citation.test.mjs:6:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/limits.test.mjs:7:import {Broker} from '../src/broker.mjs'; +packages/bus/tests/host-fixtures.mjs:6:import { registrySha256, writeGrantEvent } from '../src/broker.mjs'; +packages/bus/tests/gitea-grant.test.mjs:12:import { Broker, GRANT_EVIDENCE, writeGrantEvent } from '../src/broker.mjs'; +packages/bus/tests/csuite-messaging.test.mjs:12:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/decision-close.test.mjs:9:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/grant-scope.test.mjs:14:import { Broker, GRANT_EVIDENCE, writeGrantEvent } from '../src/broker.mjs'; +packages/bus/tests/project-await.test.mjs:7:import { Broker, BusError } from '../src/broker.mjs'; +packages/bus/tests/launcher-released.test.mjs:8:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/limit-stop.test.mjs:12:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/message-filter.test.mjs:12:import { Broker, REPLIES } from '../src/broker.mjs'; +packages/bus/tests/land.test.mjs:18:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/single-use.test.mjs:7:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/session-clear.test.mjs:10:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/grant-migration.test.mjs:10:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/usage-broker.test.mjs:7:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/project-runtime.test.mjs:13: const source = readFileSync(new URL('../src/broker.mjs', import.meta.url), 'utf8'); +packages/bus/tests/task-read.test.mjs:7:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/library.test.mjs:8:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/instances-verb.test.mjs:11:import { Broker, LIFECYCLE_HUMAN_VERBS, ORGANIZATION_KEYS, TESTER_ORGANIZATION_VERBS } from '../src/broker.mjs'; +packages/bus/tests/routes-verb.test.mjs:13:import { Broker, ORGANIZATION_KEYS } from '../src/broker.mjs'; +packages/bus/tests/broker.test.mjs:10:const { Broker } = await import('../src/broker.mjs').catch((e) => { +packages/bus/tests/grant-enroll.test.mjs:10:import { Broker, BusError, writeGrantEvent, registrySha256 } from '../src/broker.mjs'; +packages/bus/tests/hard-waits.test.mjs:7:import { Broker, ACTIONS, HARD_WAITS } from '../src/broker.mjs'; +packages/bus/tests/requirements.test.mjs:10:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/grant-executor.test.mjs:15:import { Broker, registrySha256, writeGrantEvent } from '../src/broker.mjs'; +packages/bus/tests/sessions.test.mjs:10:import { Broker } from '../src/broker.mjs'; +packages/bus/tests/user-principal.test.mjs:14: Broker, ACTIONS, USER_VERBS, TESTER_VERBS, TASK_VERBS, WEB_VERBS, HOST_VERBS, HOSTS_VERBS, CREDENTIAL_VERBS, +packages/bus/tests/user-principal.test.mjs:16:} from '../src/broker.mjs'; +packages/bus/tests/user-principal.test.mjs:137: ...ACTIONS, ...TESTER_VERBS, ...TESTER_ORGANIZATION_VERBS, ...PROJECT_FILE_VERBS, ...LIBRARY_VERBS, ...CREDENTIAL_VERBS, ...HOSTS_VERBS, +packages/bus/tests/tester.test.mjs:10:import { Broker, TASK_VERBS } from '../src/broker.mjs'; +packages/bus/tests/threads.test.mjs:9:import { Broker, THREAD_PAGE } from '../src/broker.mjs'; +packages/webui/tests/routes-browser.test.mjs:15:import {BusError} from '../../bus/src/broker.mjs'; +packages/bus/tests/creative.test.mjs:8:import { Broker } from '../src/broker.mjs'; +packages/webui/tests/granted-user.test.mjs:16:import { Broker } from '../../bus/src/broker.mjs'; +packages/bus/tests/task-page.test.mjs:7:import { Broker, TASK_PAGE, TASK_PAGE_MAX } from '../src/broker.mjs'; +packages/webui/tests/user-option-swap.test.mjs:14:import { Broker } from '../../bus/src/broker.mjs'; +packages/webui/tests/board-held.test.mjs:20:import { Broker } from '../../bus/src/broker.mjs'; +packages/webui/tests/chat-sessions-access.test.mjs:10:import { Broker } from '../../bus/src/broker.mjs'; +packages/bus/tests/review.test.mjs:14:import { Broker } from '../src/broker.mjs'; +packages/webui/tests/sso-browser.test.mjs:14:import { Broker } from '../../bus/src/broker.mjs'; +packages/bus/tests/revoke-window.test.mjs:13:import { Broker } from '../src/broker.mjs'; +packages/webui/tests/bus.test.mjs:13:import { Broker } from '../../bus/src/broker.mjs'; +packages/bus/tests/viewer.test.mjs:13: Broker, ACTIONS, USER_VERBS, VIEWER_VERBS, BOARD_VERBS, TESTER_VERBS, TESTER_ORGANIZATION_VERBS, PROJECT_FILE_VERBS, +packages/bus/tests/viewer.test.mjs:14:} from '../src/broker.mjs'; +packages/bus/tests/viewer.test.mjs:116: const others = new Set([...ACTIONS, ...TESTER_VERBS, ...TESTER_ORGANIZATION_VERBS, ...PROJECT_FILE_VERBS, ...BOARD_VERBS, 'no.such.verb']); +packages/webui/tests/tester-fixture.mjs:12:import { Broker, BusError } from '../../bus/src/broker.mjs'; +packages/webui/tests/routes.test.mjs:12:import { BusError } from '../../bus/src/broker.mjs'; +packages/webui/tests/chat.test.mjs:191: const { Broker } = await import('../../bus/src/broker.mjs'); +packages/webui/tests/bus-browser.test.mjs:11:import { Broker } from '../../bus/src/broker.mjs'; diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index b92892641..625190fcb 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -1620,3 +1620,5 @@ are never rewritten or removed; corrections are new entries. 2026-10-24T00:41Z | Duckworth (Opus 5.5) | row 323 round 1 static review on 6a0f0f4f8 (sage-3865, sage-3870) | APPROVE (static) C1 Sage full gate cli, manifest 903ef022 matches, fields sourced, tests read shipped bytes, tester mutant real, N1-N2 doc (duckworth-0300) | 2026-10-24T01:13Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row325 / #1821 Case1 project key | New goldie/row325 worktree at99930362e; pinned brief60d78e44e. Draft JSON key correction, new real-project-byte tests, sole old line70 key edit under mos-1564. Static checks only; requesting Sage CLEAR, no queue writes. 2026-10-24T01:14Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row325 / #1821 verified | sage-3882: new3/3, business264/264, one key mutant killed/restored, new3/3 again, per-run fresh shortTMP/load/helperCLEAR. Four manifest entries verify; sole old assertion key change, no queue writes. Candidate push/report to Sage and Gladstone next. +2026-10-24T04:27Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row265 / #1770 C2-09b rooms | Goldie owns build, Boyd reviews under sage-3915. New goldie/row265 worktree at ec86f2c84, claim rev3814; room verbs/parser/new tests prepared, no old tests changed. Sage-3932 slot: new parser14/14 and rooms18/18 passed; whole bus in progress before refusal mutants, stop rule active. +2026-10-24T04:35Z | Goldie (a13dd6d5-5e73-4b7d-8318-02abeff24940) | Row265 / #1770 verified | First slot stopped at project-runtime import pin. mos-1567 narrowly authorizes pin and pure-source mount correction. sage-3942 rerun: sandbox10/10, parser15/15, rooms18/18, bus957/957, twelve refusal mutants killed/restored SHA71b32ee4, parser15/15 + rooms18/18 again. Slot returned goldie-0034; no further test runs. Boyd reviews RUNNER_FILES alongside rooms; own-branch push/claimant in-review next. diff --git a/packages/bus/README.md b/packages/bus/README.md index 2b3142bae..7d5ae43e7 100644 --- a/packages/bus/README.md +++ b/packages/bus/README.md @@ -242,6 +242,9 @@ a receipt with empty `changes`. A refusal is `action.refused` with its code alon | `replies` | `{id}`: a message UUID; human and reader sessions only. The messages whose `in_reply_to` is that id, oldest first, at most 50. Writes nothing. | | `project.create` | `{name,repo?}`; human sessions only (row 121, #1615). `repo` must name one of the business's `projects` (`unknown-repo`); a second project with the same name refuses `project-exists`. Returns `{id}`. | | `project.list` | No args; human sessions only. The business's projects, oldest first. | +| `room.open` | `{project}`; human only. Creates/reuses the caller's canonical member threads, named by sorted `THREAD_DEFINITIONS` instance keys. A taken name/owner/instance refuses `room-thread-name-taken` atomically. Returns `{project,threads:[{id,instance,name}]}`. | +| `room.send` | `{project,body}`; human only, room must be fully open (`room-not-open`). Leading whitespace and `@name` tokens (including a final EOF tag) address current display names or member keys; no tags broadcasts, repeats send once. Unknown/ambiguous members refuse `unknown-addressee`/`addressee-ambiguous`; tags-only refuses `invalid-request`. Stores the original body, one normal message/route/human.input per target in one transaction, with one shared message timestamp. Returns `{ids,to}` in member-key order. | +| `room.read` | `{project,after?,limit?}`; human only, complete owned room. Oldest first across all sessions of member threads. Returns entries with first-message fields plus `thread`, `session`, `instance`, `ids` and `to`. Only consecutive human rows with equal at/body/class and distinct instances merge; repeated instances split identical sends. `limit` counts merged entries (default 50, 1–200). Continue after the last entry's last `ids` element; an `after` outside the room refuses `message-not-found`. No tester access or schema change. | | `thread.create` | `{project,name}`; human sessions only. The thread belongs to the caller and to the business's one thread instance (Projects and threads, below). Returns `{id,instance}`. Refuses `project-not-found`, `thread-exists`, `thread-instance-missing` or `thread-instance-ambiguous`. | | `thread.list` | `{project?,shared?}`; human sessions only. The caller's own threads, oldest first. With `shared: true` (row 206), the threads others own that the caller may read (Thread shares, below), each row adding `owner`. | | `thread.open` | `{thread,session?}`; human sessions only. Records a `thread_opens` row, which asks the launcher to start the thread's session. Returns `{opened}`. | diff --git a/packages/bus/src/address.mjs b/packages/bus/src/address.mjs new file mode 100644 index 000000000..d667b75ee --- /dev/null +++ b/packages/bus/src/address.mjs @@ -0,0 +1,14 @@ +// C2-09b: a pure leading-address parser, shared with the room UI's parity test. +// The remainder is for empty-body checks; the broker always stores the original input. +export function parseAddress(input) { + if (typeof input !== 'string') throw new TypeError('body must be a string'); + const tags = []; + let body = input.trimStart(); + while (true) { + const token = /^@(\S+)(?:\s+|$)/u.exec(body); + if (!token) break; + tags.push(token[1]); + body = body.slice(token[0].length); + } + return { tags, body }; +} diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs index 58dff6b09..163a7ec65 100644 --- a/packages/bus/src/broker.mjs +++ b/packages/bus/src/broker.mjs @@ -1,4 +1,5 @@ import { randomUUID, randomBytes, createHash } from 'node:crypto'; +import { parseAddress } from './address.mjs'; import { isAbsolute, dirname } from 'node:path'; import { initializeControl, processingStatus } from '../../processing-control/src/state.mjs'; import { NAME as HARNESS_NAME } from '../../plugins/src/kinds.mjs'; @@ -6418,6 +6419,99 @@ export class Broker { .all('SELECT id,at,name,repo FROM projects WHERE business=? ORDER BY seq', s.business) .filter((p) => !s.user || this.#granted(s, 'project.list', p.id, null)); } + // C2-09b: canonical per-instance threads form one human-owned project room. + case 'room.open': + case 'room.send': + case 'room.read': { + this.#human(s); + const opening = verb === 'room.open'; + const sending = verb === 'room.send'; + keys(a, opening ? ['project'] : sending ? ['project', 'body'] : ['project', 'after', 'limit'], + sending ? ['project', 'body'] : ['project']); + const project = identifier(a.project); + if (!this.#store.get('SELECT 1 FROM projects WHERE id=? AND business=?', project, s.business)) fail('project-not-found'); + const members = Object.keys(b.roles).filter((instance) => this.#threaded(s.business, instance)).sort(); + if (!members.length) fail('thread-instance-missing'); + const owner = this.#owner(s); + // Preflight all collisions/missing threads before open creates or send writes anything. + const threads = members.map((instance) => { + const t = this.#store.get('SELECT * FROM threads WHERE project=? AND name=?', project, instance); + if (t && (t.instance !== instance || t.owner !== owner)) + fail(opening ? 'room-thread-name-taken' : 'room-not-open'); + if (!t && !opening) fail('room-not-open'); + return t ?? { instance, name: instance }; + }); + if (opening) { + return { project, threads: threads.map((t) => ({ + id: t.id ?? this.#dispatch(s, 'thread.create', { project, name: t.name, instance: t.instance }).id, + instance: t.instance, name: t.name, + })) }; + } + if (sending) { + string(a.body, 32768); + const addressed = parseAddress(a.body); + if (!addressed.body.trim()) fail('invalid-request'); + const selected = new Set(); + for (const tag of addressed.tags) { + const candidates = new Set([this.#named(s.business, tag), tag].filter((instance) => members.includes(instance))); + if (!candidates.size) fail('unknown-addressee'); + if (candidates.size > 1) fail('addressee-ambiguous'); + selected.add([...candidates][0]); + } + const targets = threads.filter((t) => !addressed.tags.length || selected.has(t.instance)); + const sessions = targets.map((t) => { + const session = this.#defaultSession(t.id); + this.#sessionFence(session); + this.#checkMessaging(s.business, 'human', t.instance); + return session; + }); + const at = this.#now(); + const ids = targets.map((t, i) => { + const mid = randomUUID(); + this.#store.run( + 'INSERT INTO messages(id,at,business,from_role,from_run,to_role,class,in_reply_to,decision,corrects,body) VALUES(?,?,?,?,?,?,?,?,?,?,?)', + mid, at, s.business, 'human', 'human-cli', t.instance, 'INFO', null, null, null, a.body, + ); + this.#route(s.business, mid, t.instance, sessions[i]); + this.#event(s, 'human.input', { kind: 'instruction', message: mid }, mid); + return mid; + }); + return { ids, to: targets.map((t) => t.instance) }; + } + const limit = a.limit ?? THREAD_PAGE; + if (!Number.isInteger(limit) || limit < 1 || limit > THREAD_PAGE_MAX) fail('invalid-request'); + const tids = threads.map((t) => t.id); + const placeholders = tids.map(() => '?').join(','); + let after = 0; + if (a.after !== undefined) { + after = this.#store.get( + `SELECT m.seq FROM messages m JOIN message_threads mt ON mt.message=m.id WHERE m.id=? AND m.business=? AND mt.thread IN (${placeholders})`, + identifier(a.after), s.business, ...tids, + )?.seq; + if (after === undefined) fail('message-not-found'); + } + // At most one row per instance joins a group; one lookahead row completes the last page. + const rows = this.#store.all( + `SELECT m.*,mt.thread,mt.session,t.instance FROM messages m JOIN message_threads mt ON mt.message=m.id JOIN threads t ON t.id=mt.thread + WHERE m.business=? AND mt.thread IN (${placeholders}) AND m.seq>? ORDER BY m.seq LIMIT ${limit * members.length + 1}`, + s.business, ...tids, after, + ); + const entries = []; + let previous; + for (const m of rows) { + const entry = entries.at(-1); + if (entry && m.from_role === 'human' && previous.from_role === 'human' && m.seq === previous.seq + 1 + && m.at === previous.at && m.body === previous.body && m.class === previous.class && !entry.to.includes(m.instance)) { + entry.ids.push(m.id); + entry.to.push(m.instance); + } else { + if (entries.length === limit) break; + entries.push({ ...m, ids: [m.id], to: [m.instance] }); + } + previous = m; + } + return entries; + } // A thread belongs to the user who made it and to one declared thread instance. // Row 177 (#1675): a tester's thread is owned by `tester`, so neither reaches the other's. case 'thread.create': { diff --git a/packages/bus/src/index.mjs b/packages/bus/src/index.mjs index 3e6bea77c..c8d9951f1 100644 --- a/packages/bus/src/index.mjs +++ b/packages/bus/src/index.mjs @@ -3,3 +3,4 @@ export { startBroker } from './runtime.mjs'; export { Client } from './client.mjs'; export { views } from './views.mjs'; export { busBusiness } from './business.mjs'; +export { parseAddress } from './address.mjs'; diff --git a/packages/bus/tests/address-cases.mjs b/packages/bus/tests/address-cases.mjs new file mode 100644 index 000000000..274e66369 --- /dev/null +++ b/packages/bus/tests/address-cases.mjs @@ -0,0 +1,16 @@ +// C2-09b / C2-10c: reusable parser vectors. Keep the expected result independent of the parser. +export const addressCases = [ + ['one', '@continuity x', { tags: ['continuity'], body: 'x' }], + ['many', '@bean @chet hello', { tags: ['bean', 'chet'], body: 'hello' }], + ['repeat', '@bean @bean hi', { tags: ['bean', 'bean'], body: 'hi' }], + ['plain', 'hello @bean', { tags: [], body: 'hello @bean' }], + ['stop', '@bean hello @chet', { tags: ['bean'], body: 'hello @chet' }], + ['EOF tag', '@continuity', { tags: ['continuity'], body: '' }], + ['EOF last tag', '@bean @chet', { tags: ['bean', 'chet'], body: '' }], + ['leading whitespace', ' \t\n@bean\t@chet\nhello ', { tags: ['bean', 'chet'], body: 'hello ' }], + ['tags only', '@bean \t\n', { tags: ['bean'], body: '' }], + ['empty', '', { tags: [], body: '' }], + ['whitespace', ' \t', { tags: [], body: '' }], + ['bare at', '@ hello', { tags: [], body: '@ hello' }], + ['punctuation is a name', '@unknown, hi', { tags: ['unknown,'], body: 'hi' }], +]; diff --git a/packages/bus/tests/address.test.mjs b/packages/bus/tests/address.test.mjs new file mode 100644 index 000000000..b3c2dc337 --- /dev/null +++ b/packages/bus/tests/address.test.mjs @@ -0,0 +1,17 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { parseAddress } from '../src/address.mjs'; +import { addressCases } from './address-cases.mjs'; + +for (const [label, input, expected] of addressCases) { + test(`parseAddress: ${label}`, () => assert.deepEqual(parseAddress(input), expected)); +} +test('parseAddress rejects non-text without coercion', () => { + for (const input of [null, undefined, {}, 1]) assert.throws(() => parseAddress(input), TypeError); +}); + +// mos-1567: new coverage, separate from the pre-existing broker import pin. +test('the mounted address parser has no import dependency', () => { + assert.doesNotMatch(readFileSync(new URL('../src/address.mjs', import.meta.url), 'utf8'), /\bimport\b/); +}); diff --git a/packages/bus/tests/project-runtime.test.mjs b/packages/bus/tests/project-runtime.test.mjs index 2de4464ef..2aed8876a 100644 --- a/packages/bus/tests/project-runtime.test.mjs +++ b/packages/bus/tests/project-runtime.test.mjs @@ -14,6 +14,8 @@ test('Broker keeps the sandbox client import closure: only the listed imports, n assert.deepEqual([...source.matchAll(/^import .*;$/gm)].map(m => m[0]), [ // Row 210 (#1707): createHash gives host.run events the argv, stdin and output sha256 values. "import { randomUUID, randomBytes, createHash } from 'node:crypto';", + // Row 265 (#1770): pure room address parsing, mounted in RUNNER_FILES beside the broker. + "import { parseAddress } from './address.mjs';", // Row 65 (#1558): bindLaunch checks a run's transcript path is absolute. "import { isAbsolute, dirname } from 'node:path';", // Row 191 / sage-1367: the one additional read-only CODE source, with no project/git imports. diff --git a/packages/bus/tests/rooms.test.mjs b/packages/bus/tests/rooms.test.mjs new file mode 100644 index 000000000..4b155180a --- /dev/null +++ b/packages/bus/tests/rooms.test.mjs @@ -0,0 +1,247 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { Store } from '../src/store.mjs'; +import { Broker, THREAD_PAGE, THREAD_PAGE_MAX, TESTER_VERBS } from '../src/broker.mjs'; + +const MEMBERS = ['bean', 'chet', 'continuity']; +const shipped = (name) => JSON.parse(readFileSync(new URL(`../../../roles/${name}.json`, import.meta.url), 'utf8')); +function setup(t, { deny = false, empty = false } = {}) { + const root = mkdtempSync(join(tmpdir(), 'bus-rooms-')); + const store = new Store(root); + t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); + const role = (definition) => ({ definition, authority: { withinRole: ['message.send'], crossRole: [] } }); + // Deliberately unsorted, with two instances of one definition and a non-member chief. + const roles = { continuity: role('continuity'), chet: role('companion'), bean: role('companion'), chief: role('chief-of-staff') }; + const definitions = { + 'chief-of-staff': {}, companion: { messaging: shipped('companion').messaging }, + continuity: { messaging: deny ? { sendTo: ['human'], receiveFrom: [] } : shipped('continuity').messaging }, + }; + const b = new Broker({ store, businesses: { demo: { + id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, + roles: empty ? { chief: roles.chief } : roles, definitions, + projects: { stack: { root: '/nonexistent' } }, vars: { 'bus.tester': true }, + } } }); + const caps = { + human: b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }), + tester: b.bindTester({ business: 'demo' }), + }; + const call = (verb, args = {}, who = 'human') => b.request(caps[who], { verb, args }); + caps.chief = b.bindLaunch({ business: 'demo', role: 'chief', run: 'chief-run', harness: 'pi' }); + call('role.claim', {}, 'chief'); + const project = call('project.create', { name: 'room', repo: 'stack' }).id; + const open = () => call('room.open', { project }); + const send = (body, extra = {}) => call('room.send', { project, body, ...extra }); + const read = (extra = {}) => call('room.read', { project, ...extra }); + const effects = () => ['messages', 'message_threads', 'deliveries'].map((table) => store.get(`SELECT count(*) n FROM ${table}`).n) + .concat(store.get("SELECT count(*) n FROM events WHERE kind='human.input'").n); + const refuse = (code, fn) => { + const before = effects(); + assert.throws(fn, { code }); + assert.deepEqual(effects(), before, `${code}: no message, route, delivery or human.input`); + }; + return { b, caps, store, call, project, open, send, read, effects, refuse }; +} + +test('room.open sorts all threaded instances, creates defaults, and reuses caller-owned threads', (t) => { + const { open, call, project, store } = setup(t); + const existing = call('thread.create', { project, name: 'chet', instance: 'chet' }).id; + const result = open(); + assert.equal(result.project, project); + assert.deepEqual(result.threads.map(({ instance, name }) => [instance, name]), MEMBERS.map((x) => [x, x])); + assert.equal(result.threads[1].id, existing); + for (const thread of result.threads) { + assert.equal(store.get('SELECT owner FROM threads WHERE id=?', thread.id).owner, 'jason'); + assert.equal(call('session.list', { thread: thread.id }).filter((s) => s.default).length, 1); + } + assert.deepEqual(open(), result); + assert.equal(store.get('SELECT count(*) n FROM threads').n, 3); +}); + +for (const collision of ['instance', 'owner']) { + test(`room.open preflights ${collision} collisions atomically`, (t) => { + const { call, project, open, store } = setup(t); + call('thread.create', { project, name: 'continuity', instance: collision === 'instance' ? 'bean' : 'continuity' }, + collision === 'owner' ? 'tester' : 'human'); + assert.throws(open, { code: 'room-thread-name-taken' }); + assert.deepEqual(store.all('SELECT name FROM threads').map((r) => r.name), ['continuity']); + assert.equal(store.get('SELECT count(*) n FROM sessions').n, 1); + }); +} + +test('room verbs require the human, and none is a tester verb', (t) => { + const { call, project, open, refuse } = setup(t); + open(); + for (const verb of ['room.open', 'room.send', 'room.read']) { + const args = { project, ...(verb === 'room.send' ? { body: 'hello' } : {}) }; + assert.ok(!TESTER_VERBS.includes(verb)); + refuse('tester-refused', () => call(verb, args, 'tester')); + refuse('human-required', () => call(verb, args, 'chief')); + } +}); + +test('room verbs refuse missing projects and zero member businesses', (t) => { + const { call, refuse } = setup(t); + for (const verb of ['room.open', 'room.send', 'room.read']) { + refuse('project-not-found', () => call(verb, { project: randomUUID(), ...(verb === 'room.send' ? { body: 'hello' } : {}) })); + } + const empty = setup(t, { empty: true }); + assert.throws(empty.open, { code: 'thread-instance-missing' }); +}); + +for (const state of ['missing', 'foreign-owner', 'wrong-instance']) { + test(`send/read refuse a ${state} canonical member without effects`, (t) => { + const { call, project, send, read, refuse } = setup(t); + for (const instance of MEMBERS) { + if (instance === 'continuity' && state === 'missing') continue; + call('thread.create', { project, name: instance, instance: instance === 'continuity' && state === 'wrong-instance' ? 'bean' : instance }, + instance === 'continuity' && state === 'foreign-owner' ? 'tester' : 'human'); + } + refuse('room-not-open', () => send('hello')); + refuse('room-not-open', read); + }); +} + +test('room.send targets a key, broadcasts without tags, and preserves original text and ordinary route/input rows', (t) => { + const { open, send, store, read } = setup(t); + const threads = open().threads; + const one = send('@continuity x'); + assert.deepEqual(one.to, ['continuity']); + assert.equal(one.ids.length, 1); + assert.equal(store.get('SELECT thread FROM message_threads WHERE message=?', one.ids[0]).thread, threads[2].id); + const body = ' \t@bean @chet hello @continuity'; + const two = send(body); + assert.deepEqual(two.to, ['bean', 'chet']); + const all = send('hello @bean'); + assert.deepEqual(all.to, MEMBERS); + const rows = store.all('SELECT * FROM messages ORDER BY seq'); + assert.deepEqual(rows.map((r) => r.body), ['@continuity x', body, body, ...MEMBERS.map(() => 'hello @bean')]); + assert.equal(new Set(rows.slice(-3).map((r) => r.at)).size, 1); + assert.ok(rows.every((r) => r.from_role === 'human' && r.from_run === 'human-cli' && r.class === 'INFO')); + assert.equal(store.get('SELECT count(*) n FROM message_threads').n, 6); + assert.equal(store.get("SELECT count(*) n FROM deliveries WHERE op='routed'").n, 6); + assert.equal(store.get("SELECT count(*) n FROM events WHERE kind='human.input'").n, 6); + assert.deepEqual(read().map((r) => r.to), [['continuity'], ['bean', 'chet'], MEMBERS]); +}); + +test('current display names and repeated tags resolve once; a replaced display name is unknown', (t) => { + const { open, call, send, refuse } = setup(t); + open(); + call('name.set', { instance: 'bean', name: 'beanie' }); + assert.deepEqual(send('@beanie @bean @beanie hi').to, ['bean']); + call('name.set', { instance: 'bean', name: 'newbeanie' }); + refuse('unknown-addressee', () => send('@beanie hi')); + assert.deepEqual(send('@newbeanie hi').to, ['bean']); +}); + +test('a legacy name colliding with another member key is ambiguous; its own key resolves once', (t) => { + const { open, send, store, refuse } = setup(t); + open(); + // Naming policy now forbids key collisions; legacy data can still contain them. Append real rows. + store.run('INSERT INTO instance_names(at,business,instance,name,by) VALUES(?,?,?,?,?)', new Date().toISOString(), 'demo', 'bean', 'chet', 'fixture'); + refuse('addressee-ambiguous', () => send('@chet hi')); + store.run('INSERT INTO instance_names(at,business,instance,name,by) VALUES(?,?,?,?,?)', new Date().toISOString(), 'demo', 'bean', 'bean', 'fixture'); + assert.deepEqual(send('@bean @bean hi').to, ['bean']); +}); + +test('unknown or non-member addressees and tags-only bodies leave no message effects', (t) => { + const { open, send, refuse } = setup(t); + open(); + for (const body of ['@nobody x', '@chief x', '@bean @nobody x']) refuse('unknown-addressee', () => send(body)); + for (const body of ['@continuity', ' \t@bean\n@chet ', '', ' \t']) refuse('invalid-request', () => send(body)); +}); + +test('messaging denial on the last target rolls back the entire broadcast; allowed subset still works', (t) => { + const { open, send, refuse } = setup(t, { deny: true }); + open(); + refuse('message-sender-refused', () => send('all')); + assert.deepEqual(send('@bean @chet okay').to, ['bean', 'chet']); +}); + +test('a clearing last member fences the whole send; another active target still accepts', (t) => { + const { b, caps, open, call, send, refuse } = setup(t); + const thread = open().threads[2].id; + caps.worker = b.bindLaunch({ business: 'demo', role: 'continuity', run: 'continuity-run', harness: 'pi', thread }); + call('role.claim', {}, 'worker'); + const session = call('session.list', { thread })[0].id; + call('session.clear', { thread, session, revision: 0, op: 'room-clear', confirm: true }); + refuse('session-clearing', () => send('all')); + assert.deepEqual(send('@bean okay').to, ['bean']); +}); + +test('room.read groups one send, pages by the last grouped id, and rejects foreign or absent cursors', (t) => { + const { open, send, read, call, project, refuse } = setup(t); + const threads = open().threads; + const first = send('first'); + const second = send('@continuity second'); + const page = read({ limit: 1 }); + assert.equal(page.length, 1); + assert.deepEqual(page[0].ids, first.ids); + assert.deepEqual(page[0].to, MEMBERS); + assert.equal(page[0].thread, threads[0].id); + assert.equal(page[0].instance, 'bean'); + assert.deepEqual(read({ after: first.ids.at(-1), limit: 1 }).map((r) => r.ids), [second.ids]); + assert.deepEqual(read({ after: second.ids[0] }), []); + const other = call('project.create', { name: 'other', repo: 'stack' }).id; + const thread = call('thread.create', { project: other, name: 'bean', instance: 'bean' }).id; + const foreign = call('thread.send', { thread, body: 'elsewhere' }).id; + const noncanonical = call('thread.create', { project, name: 'side', instance: 'bean' }).id; + const side = call('thread.send', { thread: noncanonical, body: 'side' }).id; + for (const after of [foreign, side, randomUUID()]) refuse('message-not-found', () => read({ after })); + for (const limit of [0, -1, 1.1, THREAD_PAGE_MAX + 1, '1']) refuse('invalid-request', () => read({ limit })); +}); + +test('room.read keeps identical sends at the same timestamp separate and includes every session', (t) => { + const { open, call, project, store, read } = setup(t); + const threads = open().threads; + const at = '2026-10-24T00:00:00.000Z'; + const batches = [[], []]; + // Broker clocks are monotonic. Append two independently valid transaction-shaped batches to + // exercise the historical same-at boundary without altering production clocks or existing rows. + for (const batch of batches) for (const thread of threads) { + const id = randomUUID(); + batch.push(id); + const session = call('session.list', { thread: thread.id })[0].id; + store.run('INSERT INTO messages(id,at,business,from_role,from_run,to_role,class,body) VALUES(?,?,?,?,?,?,?,?)', + id, at, 'demo', 'human', 'human-cli', thread.instance, 'INFO', 'same'); + store.run('INSERT INTO message_threads(message,thread,session) VALUES(?,?,?)', id, thread.id, session); + } + assert.deepEqual(read().map((r) => r.ids), batches); + assert.deepEqual(read({ limit: 1 })[0].ids, batches[0]); + assert.deepEqual(read({ after: batches[0].at(-1) }).map((r) => r.ids), [batches[1]]); + const created = call('session.create', { thread: threads[0].id, name: 'later', op: 'new-later' }); + call('thread.send', { thread: threads[0].id, session: created.id, body: 'later session' }); + assert.equal(read().at(-1).body, 'later session'); + assert.equal(read().at(-1).session, created.id); + assert.equal(read().at(-1).thread, threads[0].id); + assert.equal(store.get('SELECT count(*) n FROM threads WHERE project=?', project).n, 3); +}); + +test('room.read defaults and caps the number of merged entries, not recipient rows', (t) => { + const { open, send, read } = setup(t); + open(); + for (let i = 0; i <= THREAD_PAGE_MAX; i++) send(`message ${i}`); + assert.equal(read().length, THREAD_PAGE); + assert.equal(read({ limit: THREAD_PAGE_MAX }).length, THREAD_PAGE_MAX); + assert.ok(read({ limit: 1 })[0].to.length === 3); +}); + +test('room.read includes an agent reply with its thread and instance without merging it with human rows', (t) => { + const { b, caps, open, call, send, read, refuse, project } = setup(t); + const thread = open().threads[0].id; + send('question'); + caps.worker = b.bindLaunch({ business: 'demo', role: 'bean', run: 'bean-run', harness: 'pi', thread }); + call('role.claim', {}, 'worker'); + const reply = call('message.send', { to: 'human', body: 'answer' }, 'worker'); + const entries = read(); + assert.equal(entries.length, 2); + assert.deepEqual(entries[0].to, MEMBERS); + assert.equal(entries[1].id, reply.id); + assert.equal(entries[1].from_role, 'bean'); + assert.equal(entries[1].thread, thread); + assert.equal(entries[1].instance, 'bean'); + refuse('human-required', () => call('room.read', { project }, 'worker')); +}); diff --git a/packages/seat/src/sandbox.mjs b/packages/seat/src/sandbox.mjs index 8ccd6ed51..c2de792cb 100644 --- a/packages/seat/src/sandbox.mjs +++ b/packages/seat/src/sandbox.mjs @@ -57,6 +57,7 @@ export const RUNNER_FILES = Object.freeze([ "packages/harness/src/pi-extension.mjs", "packages/bus/src/client.mjs", "packages/bus/src/broker.mjs", + "packages/bus/src/address.mjs", "packages/bus/src/landing.mjs", "packages/business/src/vocabulary.mjs", "packages/plugins/src/kinds.mjs",