Files

252 lines
16 KiB
Markdown

# 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.