Repository navigation
Person picker: Luke, Pauli, Shared and All #121
Description
Activity
- added a parent issue
on Oct 1, 2026 What: Delivery started on branch
feat/121-person-pickerfrom main763faa718; server and contracts writer running first, Opus UI writer next.
Why: Owner must be stamped server side at thread creation before any picker can filter.
So what: The coordinator thread runs the integrated browser pass; one PR follows with review and proof.Handoff (current)
- Workspace:
/Users/lukaszmaj/dev/toolboxmd/chromeria-worktrees/121-person-picker, branchfeat/121-person-picker, base763faa718. - Writers, one at a time: server/contracts (Prism worker) -> UI (Claude Opus 5.5) -> independent reviewer -> fixes.
- Design:
PEOPLE = ["Luke","Pauli"], ownerLukeby default. Device person is a nullablepersononauth_sessions, set in Settings -> Connections; unlabelled devices (the host desktop app) count as Luke. The ws layer overwritesowneronthread.createand bootstrap creation from the live session person.spawn_threadchild and top-level inheritthreadOwner(parent).coOwnerson the thread;thread.share/thread.unshare(owner only) /thread.leave(co-owner only), each appending an info activity so it shows in the thread. Legacy threads have no owner and count as Luke (no backfill).
Elon record
- Requirements and who asked: Person label per device, owner stamped at creation, agent threads inherit, existing threads Luke, picker Luke/Pauli/Shared/All in standard and Promachos sidebars, share/unshare/leave visible in thread (user, 2026-10-01, Issue Person picker: Luke, Pauli, Shared and All #121).
- Deleted: Access control, owners on projects, mobile, a Promachos for Pauli (Issue non-goals); configurable people names and an owner backfill migration (null owner reads as Luke).
- Bottleneck: Ownership events stamped at creation, then the integrated UI.
- Checked myself:
ThreadCreatedPayloadhas no owner (packages/contracts/src/orchestration.ts:1766); ws dispatch holds the authenticated session per connection and already stamps client origin on every command (apps/server/src/ws.ts:504-540);spawn_threaddispatchesthread.createwith the parent in hand (apps/server/src/mcp/toolkits/threads/handlers.ts:765);auth_sessionsstores client metadata (Migrations/020);AuthSessionStatehas no person (packages/contracts/src/auth.ts:348); no open PR adds a migration.
- Workspace:
What: Server/contracts ownership and sharing are implemented and pushed at
c0c61c43dc3521cfbaebc8211176290f444221fc.
Why: Device labels must stamp creation and sharing actors server side, and ownership must survive projections and live streams.
So what: The coordinator can hand this exclusive branch to the UI writer; independent review and integrated UI proof remain with the coordinator, and the baseline storage-cleanup failures below remain unresolved.Server/contracts handoff
- Workspace:
/Users/lukaszmaj/dev/toolboxmd/chromeria-worktrees/121-person-picker; branchfeat/121-person-picker. - Checkpoint
ea21e0f00was rebased ontoorigin/maine5cf87a31e82ccf086847fb277f2c33f0331f1c4without conflicts. Pushed commits:9713fb2272380b46b34c20706d11f51e22a5780eandc0c61c43dc3521cfbaebc8211176290f444221fc. - Remote head verified with
git ls-remote --heads origin feat/121-person-picker; branch clean, remote divergence0 0. No PR opened; no version changes; no dev server started; web, mobile and desktop source untouched. - Migration
055_ThreadPeopleadds nullableauth_sessions.person, nullableprojection_threads.owner, andprojection_threads.co_owners_jsondefault'[]', each with PRAGMA guards. New main still ended at 054 before this change. No owner backfill.
UI contract
- Contracts export
PEOPLE = ["Luke", "Pauli"] as const,DEFAULT_PERSON = "Luke", andthreadOwner(thread)(owner ?? DEFAULT_PERSON). Stored person names useTrimmedNonEmptyString, not enum schemas. - Read current device label through
AuthSessionState.person, fromGET /api/auth/session, including for non-admin devices. Missing person meansDEFAULT_PERSON.AuthClientSession.personalso appears in client listings andclientUpsertedaccess-stream changes. AuthSetClientSessionPersonInputis{ sessionId, person: string | null }. New auth-group endpointsetClientPerson,POST /api/auth/clients/person, returns{ updated: boolean }; requiresAuthAccessWriteScope. Typed generated client method isclient.auth.setClientPerson({ headers: {}, payload: { sessionId, person } }). No custom frontend helper added. Unknown labels return HTTP 403 withreason: "unknown_person"; null clears the label.ThreadCreatedPayload.owneris optional nullable.thread.create.ownerand bootstrapcreateThread.ownerare optional. Do not trust a client-provided owner: the ws dispatch wrapper readsSessionStore.getPerson(currentSession.sessionId)for every create/share/unshare/leave and overwrites the client stamp. Bootstrap creation uses that wrapper. Both spawn modes inheritthreadOwner(parent).OrchestrationThreadandOrchestrationThreadShellexpose optional nullableowner, andcoOwners: string[]with decoding default[]. New fixtures/local thread objects needcoOwners: []. Shared meanscoOwners.length > 0.- Dispatch through the existing orchestration RPC:
ThreadShareCommand(thread.share,{ commandId, threadId, coOwner, actor, createdAt }),ThreadUnshareCommand(thread.unshare,{ commandId, threadId, actor, createdAt }),ThreadLeaveCommand(thread.leave, same fields as unshare). Send current person as actor; ws overwrites it live. Share rejects the owner as co-owner and ignores duplicate co-owners; unshare requires owner and clears all; leave requires current co-owner and removes only that actor. These remain view operations, not access control. - State event:
thread.sharing-set, schemaThreadSharingSetPayload, fields{ threadId, coOwners, action: "shared" | "unshared" | "left", actor, updatedAt }. Apply it to thread detail state as well as shell upserts. It is included in thread-detail replay/live filtering and snapshot watermarks. - Info activity kind
thread.sharing, with summariesLuke shared this thread with Pauli,Luke stopped sharing this thread,Pauli left this shared thread. Payload carries{ action, actor, coOwners }. Duplicate shares do not append another activity. docs/fork-features.mdrecords ownership of upstream edits. Its PR value is explicitly pending; replace it with the real final PR URL when the coordinator creates that PR.
Proof on the rebased branch
pnpm exec vp test run $(git diff --name-only origin/main -- '*test.ts'): 33 files, 649 tests; 612 passed, 37 failed, all failures in the existing storage-cleanup cases ofThreadSettlementReactor.test.ts. All other 32 files passed (589 tests).- After final auth/projection adjustments:
pnpm exec vp test run apps/server/src/auth/SessionStore.test.ts apps/server/src/auth/EnvironmentAuth.test.ts apps/server/src/orchestration/people.test.ts apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts apps/server/src/mcp/toolkits/threads/spawnIntoProject.test.ts apps/server/src/persistence/Migrations/055_ThreadPeople.test.ts apps/server/src/server.test.ts: 7 files, 316 passed. Includes mid-connection relabeling, forged owner/actor replacement, bootstrap stamp, shell upserts and detail snapshots/replay for each sharing action, SQL projection/query ownership, both spawn modes, non-admin current person, live client upsert and migration rerun safety. pnpm exec vp run --filter @t3tools/contracts typecheck: passed.pnpm exec vp run --filter t3 typecheck: passed; existing Effect suggestions only.pnpm exec vp lint $(git diff --name-only origin/main -- '*.ts'): passed, existing warnings only. The newly introduced inline schema compile warning was fixed by hoisting the person predicate.git diff --checkandnode scripts/fork-features.mjs check: passed.
Baseline failure evidence
Temporarily ran the unchanged
origin/mainversion ofThreadSettlementReactor.test.tsin this same task worktree, then restored our one-field fixture addition.pnpm exec vp test run apps/server/src/orchestration/ThreadSettlementReactor.test.tsreproduced 23 passed / 37 failed, the same cases. Storage-cleanup implementation is unchanged by this branch; no cleanup checks were weakened or skipped. Decisive error:expected true to equal falsein retained-artifact/worktree existence assertions. This is not a fully green backend gate; coordinator should handle the separate cleanup baseline before claiming one.Elon record
- Requirements and who asked: Device labels, live server ownership stamps, inherited spawn ownership, nullable legacy owners, sharing and visible reverse actions, and focused backend proof (user brief and Issue Person picker: Luke, Pauli, Shared and All #121).
- Deleted: Access control, owner backfill, configurable name enums in stored events, UI work, versioning, dev servers and PR creation (explicit scope).
- Bottleneck: Server stamping and projected ownership must land before the UI writer can filter and share threads.
- Checked myself: Live session repository reads, shared normal/bootstrap client dispatch wrapper, both spawn modes, SQL migrations and projections, full detail/shell query paths, detail-event filter and watermark alignment; tests above verified relabeling on an already-open connection and stream transport.
Files changed
apps/server/src/auth/EnvironmentAuth.test.ts apps/server/src/auth/EnvironmentAuth.ts apps/server/src/auth/SessionStore.test.ts apps/server/src/auth/SessionStore.ts apps/server/src/auth/http.ts apps/server/src/git/linkCreatedPullRequest.test.ts apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts apps/server/src/mcp/toolkits/threads/handlers.testFixtures.ts apps/server/src/mcp/toolkits/threads/handlers.ts apps/server/src/mcp/toolkits/threads/mobileShell.test.ts apps/server/src/mcp/toolkits/threads/spawnIntoProject.test.ts apps/server/src/mcp/toolkits/threads/usageLimitResume.test.ts apps/server/src/mcp/toolkits/threads/wightMode.test.ts apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts apps/server/src/orchestration/Layers/ProjectionPipeline.ts apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts apps/server/src/orchestration/PullRequestSyncReactor.test.ts apps/server/src/orchestration/Schemas.ts apps/server/src/orchestration/ThreadPullRequestReactor.test.ts apps/server/src/orchestration/ThreadSettlementPolicy.test.ts apps/server/src/orchestration/ThreadSettlementReactor.test.ts apps/server/src/orchestration/commandInvariants.test.ts apps/server/src/orchestration/decider.active-order.test.ts apps/server/src/orchestration/decider.autoSettleSet.test.ts apps/server/src/orchestration/decider.pinned.test.ts apps/server/src/orchestration/decider.pullRequests.test.ts apps/server/src/orchestration/decider.questionAttachments.test.ts apps/server/src/orchestration/decider.settled.test.ts apps/server/src/orchestration/decider.snoozed.test.ts apps/server/src/orchestration/decider.titleRegeneration.test.ts apps/server/src/orchestration/decider.ts apps/server/src/orchestration/decider.turnDiffComplete.test.ts apps/server/src/orchestration/decider.userInputDismiss.test.ts apps/server/src/orchestration/messageContext.test.ts apps/server/src/orchestration/people.test.ts apps/server/src/orchestration/projector.test.ts apps/server/src/orchestration/projector.ts apps/server/src/persistence/AuthSessions.ts apps/server/src/persistence/Layers/ProjectionThreads.ts apps/server/src/persistence/Migrations.ts apps/server/src/persistence/Migrations/055_ThreadPeople.test.ts apps/server/src/persistence/Migrations/055_ThreadPeople.ts apps/server/src/persistence/Services/ProjectionThreads.ts apps/server/src/project/AgentSessionImporter.test.ts apps/server/src/provider/Layers/ProviderSessionReaper.test.ts apps/server/src/relay/AgentAwarenessRelay.test.ts apps/server/src/server.test.ts apps/server/src/ws.ts docs/fork-features.md packages/contracts/src/auth.ts packages/contracts/src/environmentHttp.ts packages/contracts/src/index.ts packages/contracts/src/orchestration.ts packages/contracts/src/people.ts scripts/fork-upstream-edits.txt- Workspace:
- added a commit that references this issue
on Oct 1, 2026 What: HTTP dispatch now stamps device people through the same live-session helper as WebSocket dispatch, pushed at
1e2e9b00706e0b77e3660af24e9e49974fbfc80e.
Why:POST /api/orchestration/dispatchpreviously trusted client-sent ownership and sharing actors.
So what: The coordinator can continue review from this SHA; all requested follow-up checks passed.- Commit:
fix(server): stamp people on HTTP dispatch, on top of UI writer HEAD5da8d1141; no fetch, pull or rebase, no web changes. - Shared helper
apps/server/src/orchestration/stampCommandPerson.tsreadsSessionStore.getPerson(sessionId)on every stamped command and defaults to Luke. Handlesthread.create.owner,thread.turn.start.bootstrap.createThread.owner, and share/unshare/leave actors. HTTP uses the session returned byrequireEnvironmentScope; ws uses its current session ID. No duplicate stamping logic. - HTTP regression added beside the existing person dispatch checks in
server.test.ts, using the real HTTP/auth harness and an engine dispatch capture. Verifies unlabelled default, forged owner, all three forged actors, bootstrap owner, and subsequent relabels on the same session. pnpm exec vp test run apps/server/src/server.test.ts apps/server/src/orchestration/people.test.ts: 2 files, 229 passed.pnpm exec vp run --filter t3 typecheck: passed (existing Effect suggestions).pnpm exec vp lint apps/server/src/orchestration/stampCommandPerson.ts apps/server/src/orchestration/http.ts apps/server/src/ws.ts apps/server/src/server.test.ts: passed (existing warnings).git diff --checkandnode scripts/fork-features.mjs check: passed. Helper and shared HTTP path recorded in fork feature map; HTTP source already allowlisted.- Push verified by
git ls-remote --heads origin feat/121-person-picker; clean worktree.
- Commit:
- added a commit that references this issue
on Oct 1, 2026 What: Rebased person ownership work onto Spectrum main and made Spectrum threads inherit their caller’s owner.
Why: Spectrum’s sharedthread.createfactory omittedowner, so a Pauli-owned caller produced threads decoded as Luke.
So what: Commitefcc583d8d93bc312036e86a9c609a141e5a4d25is pushed onfeat/121-person-picker; the coordinator can use this rebased candidate.Base:
origin/main08fc76b7b201a0c047cbc7c538f1349ce3dbafd3. Exclusive worktree:/Users/lukaszmaj/dev/toolboxmd/chromeria-worktrees/121-person-picker. Remote task branch SHA verified withgit ls-remote.The only rebase conflict was
scripts/fork-upstream-edits.txt; both lists were retained, with entries sorted within the conflicted sections. Web and client-runtime changes auto-merged; no manual edits to either directory were needed. Main still ends at migration 054, so this branch’s055_ThreadPeopleremains valid.Last commit changes:
apps/server/src/mcp/toolkits/threads/spectrum.ts: shared creation factory stampsowner: threadOwner(parent)for the Spectrum and every hidden participant.apps/server/src/mcp/toolkits/threads/spectrum.test.ts: real engine/projection regression proves Pauli ownership on the Spectrum and both participants.docs/fork-features.md: marks overlapping ChatView logic files and projection snapshot tests as shared, retaining one primary owner per file.
Inspected all Spectrum
thread.createpaths: only the shared factory constructs them; recovery replays the persisted commands, and reopen uses existing threads. The regression failed before the fix (expected null to be 'Pauli') and passes after it.Proof on the rebased candidate:
pnpm exec vp test run apps/server/src/mcp/toolkits/threads/spectrum.test.ts apps/server/src/orchestration/people.test.ts apps/server/src/mcp/toolkits/threads/spawnIntoProject.test.ts apps/server/src/persistence/Migrations/055_ThreadPeople.test.ts apps/server/src/server.test.ts apps/server/src/auth/SessionStore.test.ts apps/server/src/auth/EnvironmentAuth.test.ts apps/web/src/components/people/personView.test.ts apps/web/src/components/people/sharingTimeline.test.ts apps/web/src/components/ChatView.logic.test.ts packages/client-runtime/src/state/threadReducer.test.ts
Result: 11 files, 509 tests passed.
pnpm exec vp run --filter @t3tools/contracts typecheck pnpm exec vp run --filter t3 typecheck pnpm exec vp run --filter @t3tools/web typecheck
All passed; server emitted existing Effect suggestions.
Targeted lint:
pnpm exec vp lintwith the 104.ts/.tsxpaths returned bygit diff --name-only origin/mainpassed (warnings, no errors; no automatic fixes). Format check passed for the three final-commit files.git diff --check origin/main HEADpassed.node scripts/fork-features.mjs checkpassed for 22 features.Node and
pnpm exec node: v26.10.0. The boolean-bind retirement-query failure did not occur, so no unchanged-main reproduction was needed.Push:
git push --force-with-lease -u origin feat/121-person-picker, remote SHA verified, worktree clean. No dev server, version bump, PR, live-state writes, or canonical/other-worktree edits.Requirements and who asked: User requested Spectrum caller ownership and rebased focused proof for #121.
Deleted: No additional behavior or migration was introduced.
Bottleneck: One shared Spectrum creation factory omitted the owner stamp.
Checked myself: Failing-before regression, 509 passing tests, three typechecks, targeted lint, fork map, migration numbering and remote SHA.Independent review: reviewer thread
sub.sub.sub.sub.46b03420-8643-4703-8756-8602f14b39fb.dispatcher-b312028f9734.dispatcher-c91d455e8fc6.worker-ef68f932cb44.reviewer-162e9bc16a36approved exact SHAefcc583d8d93bc312036e86a9c609a141e5a4d25with no blocking findings. Its initial pre-fix-outbox finding was withdrawn against #121’s explicit legacy absent-owner default and no-backfill contract. The reviewer did a read-only source review and did not rerun proof.What: Draft PR #131 is open at exact head
091cfdb182ce27770712e871bae14e63ddad9cae, independently approved, with browser proof except the unsent-draft check.
Why: The rebase onto #117 and #118 and the Spectrum owner inheritance are done; the browser host dropped twice, which delayed the integrated pass.
So what: The coordinator thread finishes the draft check, uploads the screenshots and marks the PR ready; no merge or install yet.Handoff (current, supersedes the earlier coordinator comment)
- PR: feat: person picker and shared threads for Luke and Pauli #131 (draft, linked). Branch
feat/121-person-picker; base08fc76b7b; head091cfdb18, which isefcc583d8plus the fork-map PR URL. - Review:
review/independentsuccess on091cfdb18(reviewer thread…reviewer-fb7373f5ac1e, cumulative re-review); an earlier approval covered1e2e9b007. - Proof: 509 focused tests, typechecks and lint per the rebase handoff. Baseline limits not from Person picker: Luke, Pauli, Shared and All #121: 37
ThreadSettlementReactorstorage-cleanup failures, and on Node 24.13.1 the boolean bind in main's thread-retirement query. - Browser, isolated
.t3, two paired sessions: labelling, stamping, picker views, Promachos picker, Share, Leave and Unshare pass. The unsent-draft check is open. Screenshots are in/tmp/promachos-coord/evidence/121/, waiting for the coordinator to upload them. - No versioning by design. Next: upload the evidence and mark ready; the coordinator merges and runs the installed-build checks.
- PR: feat: person picker and shared threads for Luke and Pauli #131 (draft, linked). Branch
What: PR #131 is ready for review at exact head
091cfdb182ce27770712e871bae14e63ddad9cae; the integrated browser pass, including the unsent-draft check, passed on that head.
Why: All acceptance criteria now have test and browser evidence on the final candidate.
So what: The coordinator uploads the screenshots in/tmp/promachos-coord/evidence/121/, merges, and runs the installed-build check with Pauli's device.Handoff update (supersedes the open items in the previous comment)
- Unsent-draft check passed: B's draft shows under Pauli and All and not under Luke, and it persists across reload.
- Cost: UNKNOWN because of an Agent Observer attribution gap (feat: person picker and shared threads for Luke and Pauli #131 (comment)).
- Open: the screenshot upload (no upload tool was available), the merge, and the installed-build check. No source changes are pending.
What: PR #131 now creates the people columns at runtime instead of as migration 55. The current head is
7446161c9a45fed5fdb3167ad324a2d057e52a20, independently approved.
Why: Coordinator requirement: a fork entry in upstream's numbered migration list would make the user's live database skip upstream's future migration with the same ID.
So what: The coordinator uploads the screenshots and merges; nothing more is needed from this thread.Handoff (current, supersedes earlier heads)
- Head
7446161c9=091cfdb18plusfix(server): create people columns at runtime, not as migration 55.Migrations.tsis identical toorigin/main, and the 055 files are deleted. - Startup order: fork-owned
ensureThreadPeopleSchema(the same PRAGMA-guarded ALTERs) runs inpersistence/Layers/Sqlite.tssetup immediately afterrunMigrations(), before any auth or projection consumer. - Proof:
- Writer: 26 files, 364 tests; contracts and server typechecks; lint; fork map (Node v26.10.0).
- Coordinator thread, independently:
forkThreadPeopleSchema.test.ts3/3 passed on the exact head. A future upstream 55 executes, setup runs before reads, and a rerun keeps data.
- Review:
review/independentsuccess on7446161c9(reviewer…reviewer-09e459dd73dc, feat: person picker and shared threads for Luke and Pauli #131 (comment)). - UI is unchanged since the browser pass. Cost is UNKNOWN (attribution gap). No versioning by design. Open for the coordinator: screenshot upload, merge, installed-build check.
- Head
- added a commit that references this issue
on Oct 1, 2026
What: Chromeria knows whose device is whose and shows a sidebar picker for Luke, Pauli, Shared and All, with a Share toggle on threads.
Why: Pauli will use the user's machines from her own devices, and T3 shows every paired device every thread with no notion of people.
So what: The coordinator builds it after #116 lands; the user tests it with Pauli's device.
Parent
#113
Outcome
Each paired device carries a person label set in Settings → Connections (the host's own desktop app is the owner's). Every new thread belongs to the person whose device started it; threads started by agents belong to the owner of the thread that started them; existing threads belong to the user. A sidebar picker shows Luke, Pauli, Shared or All. A Share toggle on a thread adds the other person as a co-owner and puts a Shared label on it, so it shows under both people's views and under Shared; both can read and write in it. The creator can unshare, which removes co-owners and the label; a co-owner can leave, which removes only them from the thread. The creator always keeps the thread, and both actions show in the thread. It is a view, not access control.
Elon record
packages/contracts/src/orchestration.ts; paired sessions are already listed in Settings → Connections;docs/internals/remote.md(a project and its threads belong to one environment).Acceptance criteria
Non-goals
Blocked by
Required proof