Files

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; 631a256ba pushed 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, tip 3410df7c3 (morgana-0144, range-diff = for all three). 206 is rebased onto 3410df7c3 as 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 is 17f415792. 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 #threadRows reads one row per session, and idle; no share, read or grant code), sessions.test.mjs (269: refusedOnce checks 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 since 48e450ce8, 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 #userGrants keeps 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 userLevel asks collaborator and a viewer is refused each access-refused (design 4.3).
    • #threadRead's owner path passes thread.messages to #grantedThread, so a viewer owner reads its own thread at the viewer level (duckworth-0169 C1). Share and unshare pass verb; shares passes thread.shares.
    • Two conflicts, both in broker.mjs: the USER_VERBS block and the thread.messages line (275's #grantedThread(s, 'thread.messages', #thread(...)) gives way to #threadRead, which now holds that check). user-principal.test.mjs merged clean.
  • 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_shares as design 5.2, its index and three append-only triggers, in schema.sql and in the new step src/migrations/3l-3m.sql with the same SQL text. SCHEMA_SHA256 d5ca9bb9268994b175ac79d38939be4bb7f2e80bc0d9ea273eedaae50c29b8a9, SCHEMA_VERSION 3m. tests/fixtures/schema-3l.sql is the 3l schema.sql (sha256 cd68512212ce…).
  • thread_shares joins timedTables (store.mjs), the broker's clock list, COUNTED in scripts/stack-release.sh and LISTED in stack-release.test (T46). It has at, so it goes in the counted list, not the untimed one.
  • broker.mjs:
    • #readable(s, t) holds rules 1-4 of design 5.4. #shared reads the latest row per thread and target. #covered reads the access document once per call and covers nothing on a read failure, as #userGrants does.
    • #threadRead admits the owner (with #grantedThread) or #readable, and serves only thread.messages. #thread keeps the owner check for every write verb.
    • thread.share, thread.unshare, thread.shares (design 5.3). thread.list {shared:true} adds owner; the plain list keeps its shape (5.5).
    • replies leaves out user thread messages except those in threads #readable admits. trail needed no change: it already filters through #hiddenMessage, which calls #readable.
    • USER_VERBS gains the three share verbs. thread.shares joins #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)

  1. Label 3l→3m, not 3i-3j (design) or 3l (brief title). 264a took 3k-3l, and labels go by landing order (sage-2789).
  2. A share covers every session of the thread, and session.list stays 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 (human thread-not-owned, reader read-only, user access-refused). Follow-up, bound to whichever of row 193 or the share wiring lands second: wire the sharee's session.list. The bar is T1 sessions line 7 and REQ-USER-4 (candidate PRD 1509c7cc5, docs/prd/mosaic-stack.md:1027-1052): read-only, and it needs current grants, the thread owner's included.
  3. 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. #readable now 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.
  4. Access is read per request (sage-2818 Q3). Design 5.6's "30 s timer" and access.reload have no path at this base: runtime.mjs:263 passes access: () => 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.
  5. 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.
  6. An unshare checks no coverage, so an owner can end a share whose target lost its grant.
  7. thread.shares for a user goes through #grantedThread at 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, replace this.#thread(s, a.thread) with this.#threadRead(s, a.thread). Killed by the first test (bob's send to A must refuse thread-not-owned).
  • M8. In thread.share, delete the share-not-covered line. Killed by "mutant 8" (the share to user:carol must refuse).
  • M9. In thread.share/thread.unshare, replace this.#thread(s, a.thread) with this.#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 and access.reload case binds the row that adds access.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 passes thread.messages to 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).
  • Notes, no change asked: N1 #readableThreads binds a reader's owner as NULL (harmless); N2 replies reads the access file once per shared thread; N3 a share to a user absent from users is stopped only by coverage; N4 the gate evidence goes in "Runs" below before in-review.
  • Round 2 on d4fbce501 (rebased on 275 r3 957a9bfe0): C1 and C2 met, round 1 APPROVE carries (Duckworth, duckworth-0175, 17:32Z, note agents/duckworth/work/row206/R2-C1C2-d4fbce501.md on duckworth/reviews cd0fb90e2). 4ae94c5c9 is 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 at 3410df7c3, on 253's 48e450ce8): C3 met, approve carries (Duckworth, duckworth-0179, note agents/duckworth/work/row206/R3-C3-670658dd1.md, 01f29379b). Pushed as launchpad/row206-48e4 at 670658dd1 in 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 at 9538e0fc8); 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 * test
    M3 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.