16 KiB
Row 206 build notes (Launchpad): 193b2, thread shares (#1704)
Brief: docs/plans/2026-10-20_human-users-build.md, "Brief: 193b2, thread shares (schema 3l)"
(blob de3a6310b). Design: agents/morgana/work/queue-193/DESIGN.md at e2d161e2e (sha256 dd7fcb41…),
sections 4.3, 5.2-5.6 and 8. Reviewer: Duckworth. Assignment: sage-2789.
Base and chain
- Started on
aca16404c(sage-2789) on branch launchpad/row206;631a256bapushed there (sage-2854). - Round 2: rebased onto Morgana's 275 r3,
957a9bfe0(morgana-0143, which carries duckworth-0166 B1), on the new branch launchpad/row206-r275. No force push of the old branch. - Round 3 (C3): 253 landed at
48e450ce8. Morgana rebased 275 onto it as morgana/row275-48e4, tip3410df7c3(morgana-0144, range-diff=for all three). 206 is rebased onto3410df7c3as launchpad/row206-48e4. Range-diff against the round 2 commits is=for all five, and the 22 manifest blobs equal round 2's (e4dba2a84). C3 sweep: outside agents/ and docs/, 253 changed BUILD-LOG.md, packages/cli/tests/{case-sudo-fixture.mjs, case1-accept.test.mjs, case2-unit.test.mjs}, packages/webui/README.md and scripts/{case1-accept.sh, case2-unit.sh}. None is a 275 or 206 path, and no bus test, stack-release.test or stack-release.sh reads them. - Round 4 (slot rebase, sage-2959): 275 landed, then 269 (
efcc9ea8d) and 282 (647424442), and refactor is17f415792. 206 is rebased onto it as launchpad/row206-17f4. 275's three commits drop out as already applied, and the seven 206 commits apply clean. Sweep 3410df7c3..17f415792 over packages/ and scripts/: broker.mjs (269: the launcher's#threadRowsreads one row per session, andidle; no share, read or grant code), sessions.test.mjs (269:refusedOncechecks actor_run; 206's hunks are the three '3l'→'3m' pins at T7 and engine3k, which still apply), end-launch.test.mjs, cli launcher and session-launch tests, seat session and sandbox, and tasks requirements. None reads a share table or the schema version. schema.sql, store.mjs, the migration tests, schema.test, stack-release.sh and stack-release.test are unchanged since48e450ce8, so SCHEMA_VERSION on refactor is still '3l', the label 3l→3m stands, the table and trigger pins and COUNTED/LISTED lists stand, and fixtures/schema-3l.sql still equals refactor's schema.sql byte for byte. engine3k's strip regex starts at 264a's step comment and ends at the list's close, so it cuts 206's step too, as before. - 275 gives
VIEWER_VERBS,userLevel(verb)and verb-aware#granted/#grantedThread, and#userGrantskeeps a live grant of any level, so a viewer binds. On the rebase:- USER_VERBS keeps 275's block and gains the three share verbs. They are not in VIEWER_VERBS,
so
userLevelasks collaborator and a viewer is refused eachaccess-refused(design 4.3). #threadRead's owner path passesthread.messagesto#grantedThread, so a viewer owner reads its own thread at the viewer level (duckworth-0169 C1). Share and unshare passverb; shares passesthread.shares.- Two conflicts, both in broker.mjs: the USER_VERBS block and the
thread.messagesline (275's#grantedThread(s, 'thread.messages', #thread(...))gives way to#threadRead, which now holds that check). user-principal.test.mjs merged clean.
- USER_VERBS keeps 275's block and gains the three share verbs. They are not in VIEWER_VERBS,
so
- C2 sweep on
957a9bfe0: 275 touches agents/morgana/work/queue-275/BUILD.md, broker.mjs, user-principal.test.mjs and viewer.test.mjs. Overlap with 206 is broker.mjs and user-principal.test.mjs, both handled above. viewer.test.mjs has no schema-version or hash pins, and its "every verb off the user list" test reads USER_VERBS, so the share verbs drop out of it on their own. - The live
--migrate(3l→3m on the running bus) is not part of this build. It is Mos's go under Q173, with a backup first and a STATE report after (mos-1310 item 4).
What changed
- Schema 3l→3m.
thread_sharesas design 5.2, its index and three append-only triggers, in schema.sql and in the new stepsrc/migrations/3l-3m.sqlwith the same SQL text. SCHEMA_SHA256 d5ca9bb9268994b175ac79d38939be4bb7f2e80bc0d9ea273eedaae50c29b8a9, SCHEMA_VERSION 3m.tests/fixtures/schema-3l.sqlis the 3l schema.sql (sha256 cd68512212ce…). thread_sharesjoinstimedTables(store.mjs), the broker's clock list,COUNTEDin scripts/stack-release.sh andLISTEDin stack-release.test (T46). It hasat, so it goes in the counted list, not the untimed one.- broker.mjs:
#readable(s, t)holds rules 1-4 of design 5.4.#sharedreads the latest row per thread and target.#coveredreads the access document once per call and covers nothing on a read failure, as#userGrantsdoes.#threadReadadmits the owner (with#grantedThread) or#readable, and serves onlythread.messages.#threadkeeps the owner check for every write verb.thread.share,thread.unshare,thread.shares(design 5.3).thread.list {shared:true}addsowner; the plain list keeps its shape (5.5).repliesleaves out user thread messages except those in threads#readableadmits.trailneeded no change: it already filters through#hiddenMessage, which calls#readable.- USER_VERBS gains the three share verbs.
thread.sharesjoins#processingGate's read list.
- packages/bus/README.md: the verb rows, a "Thread shares" section, and the schema line (v3m, the new sha, 23 tables, 90 triggers). schema.test counts 89 triggers because it drops one first.
Choices and departures (for Sage and Duckworth)
- Label 3l→3m, not 3i-3j (design) or 3l (brief title). 264a took 3k-3l, and labels go by landing order (sage-2789).
- A share covers every session of the thread, and
session.liststays owner-only (sage-2818 Q1, from Sage's Q7 ruling, agents/sage/work/queue-264/RULINGS.md:72-77). broker.mjs keeps its "no shares (Q7)" refusal: a sharee reads any session it can name but can't list them. A test holds the refusal (humanthread-not-owned, readerread-only, useraccess-refused). Follow-up, bound to whichever of row 193 or the share wiring lands second: wire the sharee'ssession.list. The bar is T1 sessions line 7 and REQ-USER-4 (candidate PRD1509c7cc5, docs/prd/mosaic-stack.md:1027-1052): read-only, and it needs current grants, the thread owner's included. - A reader session matches shares as the business human (sage-2818 Q2: accepted, for the human
principal only). Design 4.3 says reader sessions "stay owner-only", and 5.5 says "messages in
other users' unshared threads hidden" for human, reader and tester. The reader sees what the
owner sees, shares to the owner included. Q2 also found a gap in the first tip (
16b9c6df6): an agent session has no owner, but it matched a*share, so a shared user message showed in an agent's trail.#readablenow returns false unless the session is the business human, a reader or a user. A test binds an assistant run to the shared thread and a chief run with no thread, and checks that neither (nor the tester) sees the message after a*share and a share to the owner. - Access is read per request (sage-2818 Q3). Design 5.6's "30 s timer" and
access.reloadhave no path at this base: runtime.mjs:263 passesaccess: () => loadAccess(file, context), read on every request. The tests cover the per-request read (removed, expired, out of scope and unreadable), which is the stricter case. - Rule 3 asks the owner's grant for any level, like rule 2 (design: "still has a live grant covering t"). A user owner lowered to viewer keeps the thread readable to its sharees.
- An unshare checks no coverage, so an owner can end a share whose target lost its grant.
thread.sharesfor a user goes through#grantedThreadat the collaborator level, like share and unshare, so a viewer is refused it. Design 4.3 lists shares as "refused" for viewers.
Tests
New: tests/thread-shares.test.mjs, 16 tests: the read flow and the write refusals, *, rule 2,
rule 3 (with trail for human and reader), latest-row state, share-not-covered, target checks,
lost owner coverage, tester and reader refusals (18 action.refused events, no rows), trail and
replies, sessions, the sharee's session.list refusal (Q1), agents and the tester matching no
share (Q2), the viewer column (C1: a viewer reads a share, its share verbs refuse,
and a viewer target binds and reads), the 3l→3m migration and the 3l engine's refusal of a 3m store.
Pins moved 3l→3m: account-, chief-of-staff, comment-, gitea-, grant-, name-, spec-, usage- and
thread-migration, sessions, schema (23 tables, 89 triggers) and user-principal (USER_VERBS).
The engine copies in account-, gitea-, grant- and comment-migration gain 3l-3m.sql;
comment-migration's future step becomes 3m→3n. later3l in account-, gitea- and grant-migration
also leaves out thread_shares objects, and the gitea and grant rows() lists leave out the
table, since each compares a store migrated through 3m against an older one.
Mutants (design section 8 numbers)
Each runs on a copy of the candidate tree with one edit, under timeout -k 10 300. The named
test must fail on an assertion; a thrown error or a refusal before the assertion doesn't count.
- M2. In
#readable, delete the rule 2 line (if (s.user && !this.#covered(s.user, ...)) return false;). Killed by "mutant 2: a share never widens a grant" (bob reads after his grant moves to p2) and by the*test (carol reads). - M3. In
#readable, delete the rule 3 line. Killed by "mutant 3" (the human reads A after alice's grant is removed). - M4. In
#shared, replace the latest-row lookup with any share row:SELECT op FROM thread_shares WHERE thread=? AND target=? AND op='share' LIMIT 1. Killed by "mutant 4" (bob still reads after the unshare). - M7. In
thread.send, replacethis.#thread(s, a.thread)withthis.#threadRead(s, a.thread). Killed by the first test (bob's send to A must refusethread-not-owned). - M8. In
thread.share, delete theshare-not-coveredline. Killed by "mutant 8" (the share to user:carol must refuse). - M9. In
thread.share/thread.unshare, replacethis.#thread(s, a.thread)withthis.#threadRow(s, a.thread). Killed by the first test (bob's share of A to*must refuse). - Q2 probe (not a design mutant). In
#readable, delete the human-principal line (if (!s.human && !s.reader && !s.user) return false;). Killed by "only a human principal matches a share" (the agent's trail shows the message after the*share).
Run limits
From 264's table (agents/gizmoduck/work/queue-264/BUILD.md, Run limits), each at least 3x the longest recorded run. A timeout with no failed test goes to Sage first; a timeout in the shell phase goes to Mos unplaced (mos-1275).
| Run | LIMIT |
|---|---|
| stack-release.test alone | 1800 s |
| each other touched file alone | 300 s |
| bus suite | 900 s |
| cli suite | 1800 s |
| each scripts/test-*.sh | 900 s |
| each mutant probe | 300 s |
Review
- Manifest aca16404c..631a256ba: APPROVE, no blockers (Duckworth, duckworth-0169, 16:57Z,
note agents/duckworth/work/row206/REVIEW-manifest-631a256ba.md on duckworth/reviews
84458606d). Static only. Departures 1-7 accepted. Departure 1: whichever of 206 and row 271 lands second relabels its step. Departure 4: the 30 s timer andaccess.reloadcase binds the row that addsaccess.reload(193c). - Conditions for the 275 rebase round:
- C1, design 4.3's viewer column: share, unshare and shares each refuse a viewer; a viewer
reads a thread shared with it;
#threadRead's owner path passesthread.messagesto 275's verb-aware#grantedThread. - C2: rerun the drift and pin sweeps on the rebased base (253 and 275 touch broker.mjs; 275 also touches the user-principal USER_VERBS pin).
- C1, design 4.3's viewer column: share, unshare and shares each refuse a viewer; a viewer
reads a thread shared with it;
- Notes, no change asked: N1
#readableThreadsbinds a reader's owner as NULL (harmless); N2 replies reads the access file once per shared thread; N3 a share to a user absent fromusersis stopped only by coverage; N4 the gate evidence goes in "Runs" below before in-review. - Round 2 on
d4fbce501(rebased on 275 r3957a9bfe0): C1 and C2 met, round 1 APPROVE carries (Duckworth, duckworth-0175, 17:32Z, note agents/duckworth/work/row206/R2-C1C2-d4fbce501.md on duckworth/reviewscd0fb90e2).4ae94c5c9is patch-identical to 631a256ba; manifest 22 paths. - C3, for the post-253 rebase: rerun the drift and pin sweep against 253's tip, and push the branch so the manifest is read from the pushed tip. N4 still applies.
- Round 3 on
670658dd1(rebased on 275 at3410df7c3, on 253's48e450ce8): C3 met, approve carries (Duckworth, duckworth-0179, note agents/duckworth/work/row206/R3-C3-670658dd1.md,01f29379b). Pushed as launchpad/row206-48e4 at670658dd1in sage-2886's gap, after Morgana's morgana/row275-48e4; Duckworth's ls-remote matched the tip (duckworth-0180).
Runs
Slot from sage-2959 (CLEAR 19:13Z 10-22). Claimed rev 3317. Everything ran on launchpad/row206-17f4
on refactor 17f415792, after npm ci --ignore-scripts from the lockfile (oauth4webapi 3.8.8, no
symlinked node_modules). Each run had TMPDIR from mktemp -d /mnt/storage/scratch/tmp/gXXXXXX and
timeout -k 10 at the limits above. Logs are in /mnt/storage/scratch/launchpad-r206/runs/.
-
First-run red, placed under mos-1272 (Sage accepted it, sage-2960). On
1a228f395, thread-shares.test.mjs "the owner's trail and replies..." failed with ERR_SQLITE_ERROR "a threaded message's session is in its thread". The test inserted the reply's message_threads row with session = the thread id, but a fresh thread's default session has its own id (264a; only a migrated thread's reuses the thread's). The fix (9538e0fc8) reads the session from session_defaults. It changes one fixture line in a new test, with no product change and no assertion changed. -
Each touched file, whole and alone. The first 13 ran on
1a228f395(the code under test is the same at9538e0fc8); thread-shares reran on the fix.File Pass/fail thread-shares 16/0 on 9538e0fc8(15/1 before the fix)schema 1/0 sessions 7/0 user-principal 22/0 account-migration 3/0 chief-of-staff 42/0 comment-migration 5/0 gitea-migration 5/0 grant-migration 6/0 name-migration 1/0 spec-migration 1/0 thread-migration 3/0 usage-migration 1/0 cli stack-release.test 218/0, 580 s -
Mutants on
9538e0fc8(19:28Z). Each is one exact edit on a copy of the tree (the edit must match once), running thread-shares.test.mjs. All are killed on ERR_ASSERTION in the named test:Mutant Result Failing tests Assertion message M2 14/2 "mutant 2" and the *testM3 15/1 "mutant 3" "removed" M4 14/2 "mutant 4" and the trail test "human" M7 15/1 the first test "thread.send" M8 15/1 "mutant 8" "user:carol" M9 14/2 the first test and "a share target is..." "thread.share" Q2 probe 15/1 "only a human principal" "agent after" -
Suites on
9538e0fc8, 19:28:23Z to 19:40:25Z, all rc=0:Suite Pass/fail bus 850/0 cli 869/0 test-auth 15/0 test-conductor 17/0 test-config 31/0 test-discord 81/0 test-extension-package 18/0 test-foundation 44/0 test-land-work 86/0 test-queue 156/0 test-release 14/0 test-task 91/0 The skip lines are by design. test-queue skips "verify and render --check" because the worktree isn't the canonical root. test-task skips the "live provider cases" twice (row 244: no provider call in the gate). No Docker group skipped.
-
Result: GREEN.