fix(agents): list_sessions discovers shared workspaces; redact foreign private threads - #2341
Conversation
…n private threads Discovery symmetry (PR #2336's flagged asymmetry): spawn_session's explicit- workspaceId path admits drive MEMBERS into a shared workspace (checkSessionAccess), but list_sessions only enumerated workspaces the caller OWNS — a member could target a shared workspace by id yet never learn the id. list_sessions now carries a distinct `sharedWorkspaces` section: other members' sessions in drives the caller belongs to, gated per-row by the SAME pure decision the spawn gate uses (decideAgentSessionAccess fed by resolveDriveMembership — never a second predicate), bounded by an explicit member-visible cap (MAX_MEMBER_VISIBLE_WORKSPACES = 100, newest activity first; unlike the own set, nothing structural caps this one). Shared-metadata redaction (issue #2262 finding 6, conservative product decision flagged for veto in the PR): in listings of a workspace the viewer does not OWN, conversations that are neither the viewer's own nor isShared keep their row (agent + activity time) but read "(private thread)". The owner sees everything in their own workspace, unchanged. One pure mechanism: redactConversationTitleForViewer (packages/lib/src/agent-sessions/redact-conversation-listing.ts), routed through both viewer-facing listing mappings (listWorkspaceWorkers, listSharedWorkspaces); rule documented on listSessionConversationsBulk, where the queries live. The list_sessions description (and its pinned schema-test literal) changes DELIBERATELY in this commit — a contract edit, not drift — and narrows the addressability claim to workers the caller owns. Spec doc gains axioms 6-7. Tests: redaction rule unit suite (packages/lib); listSharedWorkspaces runtime suite running the REAL pure gate + redaction over mocked IO (member discovery / non-member nothing / redacted titles with honest count / the 100 bound); contract tests pin the sharedWorkspaces section and discovered-id-is-spawnable; a real-Postgres integration test proves member discovers AND spawns into a shared workspace with redacted titles while a non-member gets neither. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnzUuAixdTJ8xDpP5S92Ts
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Phase 2 of the Agent-Session Single Source of Truth epic: closes the discovery asymmetry PR #2336 explicitly flagged and deliberately deferred, and lands the shared-metadata decision from issue #2262 finding 6.
1. Discovery symmetry —
list_sessionsnow discovers shared workspacesPR #2336 flagged: "
list_sessions's cross-workspace listing is scoped to workspaces the caller OWNS, whilespawn_session's explicit-workspaceId path (correctly) also permits drive members into a shared workspace — so a member can target a shared workspace by id but won't discover it vialist_sessions."Fixed by a sibling dep,
listSharedWorkspaces(session-tools-runtime.ts): other members' sessions in drives the caller belongs to, reported as a distinctsharedWorkspacessection of thelist_sessionsoutput so the model always knows which workspaces are its own and which are shared.workspaceIdpath (checkSessionAccess→decideAgentSessionAccess, fed by the sameresolveDriveMembershipgather —checkAccessForSubject's composition). Candidate drives come fromgetDriveIdsForUser, deliberately over-broad (page-permission-only drives resolve to membership'none'and are denied by the one real gate) rather than a hand-rolled narrower query that could drift from it.MAX_ACTIVE_WORKSPACES_PER_OWNERspawn ceiling (a listing can never truncate it). The member-visible set has no structural ceiling behind it (N members × their own caps), so it carries its own documented bound:MAX_MEMBER_VISIBLE_WORKSPACES = 100, newest activity first — a real truncation bound, documented at the constant.list_sessions' tool description now names thesharedWorkspacessection, the redaction marker, and narrows the addressability claim to workers the caller owns (foreign workers always read as nonexistent to the verbs — unchanged behavior, previously overclaimed). The pinned literal insession-tools-schema.test.tsis re-pinned in the same commit, with a comment marking it as a contract edit.docs/2.0-architecture/agent-sessions.md§2 gains axioms 6–7 in the same PR, per §5's rule.2. Shared-metadata decision (issue #2262 finding 6)
Shared-workspace listings showed titles of ALL conversations in the workspace — including other members' private threads. The conservative rule now in force:
conversations.isShared); every other row keeps its row — agent id + activity time, so the orchestration signal and the listing count stay honest — with the title replaced by the fixed marker(private thread).openOwnSession— unchanged).Implemented in the listing mapping layer in one place: the pure
redactConversationTitleForViewer(fail-closed), routed through both viewer-facing mappings (listWorkspaceWorkersfor the caller's current workspace — which can be a shared one they were spawned into — andlistSharedWorkspaces). The rule is documented where the queries live (listSessionConversationsBulk's doc, per the issue's own ask), andSessionConversationEntrynow carriesownerId/isSharedso mapping layers apply the rule without a second query. The sidebar/API surfaces only ever enumerate the caller's own sessions, where the owner rule makes redaction a no-op.This is a product decision, embedded conservatively and flagged for veto: if product prefers full titles for drive members (shared-workspace-semantics-all-the-way) or full redaction, the change is one function + its pinned tests.
Tests
packages/lib/src/agent-sessions/__tests__/redact-conversation-listing.test.ts— the rule, including fail-closed degenerate inputs.session-tools-runtime.test.ts—listSharedWorkspacesruns the REAL pure gate + redaction over mocked IO: member discovery with own/current-workspace exclusion; non-member sees nothing (and the'none'drive is never queried); redacted titles with honest count and surviving activity time; the 100 bound, newest first.session-tools-contract.test.ts— thesharedWorkspacessection and "a workspaceId discovered in sharedWorkspaces is spawnable-into".session-discovery-symmetry.integration.test.ts(real migration-built Postgres) — a drive member discovers the owner's shared workspace, sees own + shared titles,(private thread)for the owner's private one (count honest, real title nowhere in the payload), and spawns into the discovered id; the owner sees everything unredacted; a non-member's listing and spawn both refuse — the same one decision.session-tools.test.ts/session-tools-schema.test.tsupdated for the new dep, the viewer parameter, and the re-pinned description.Gates (run sequentially, real results)
bun run lint— 14/14 tasks green.bun run knip:check— 4 issues, all within baseline (new lib module added to knip entries; the new bound stays module-local).bun run typecheck— 16/16 tasks green.src/lib/ai/tools/__tests__/+src/lib/agent-sessions/__tests__/, DB on :5433): 54 files, 1251 tests passed, including both integration suites.bun run test:unit— exit 0; web: 1,090 files passed, 16,167 tests passed (1 file / 6 tests skipped, pre-existing).🤖 Generated with Claude Code
https://claude.ai/code/session_01XnzUuAixdTJ8xDpP5S92Ts