Skip to content

fix(agents): list_sessions discovers shared workspaces; redact foreign private threads - #2341

Merged
2witstudios merged 1 commit into
pu/broken-sessionsfrom
fix/session-discovery-symmetry
Aug 6, 2026
Merged

fix(agents): list_sessions discovers shared workspaces; redact foreign private threads#2341
2witstudios merged 1 commit into
pu/broken-sessionsfrom
fix/session-discovery-symmetry

Conversation

@2witstudios

Copy link
Copy Markdown
Owner

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.

⚠️ Product decision embedded: redaction of other members' private-thread titles in shared workspaces — veto/adjust before or after merge; the mechanism is one function (redactConversationTitleForViewer, packages/lib/src/agent-sessions/redact-conversation-listing.ts). Details in §2.

1. Discovery symmetry — list_sessions now discovers shared workspaces

PR #2336 flagged: "list_sessions's cross-workspace listing is scoped to workspaces the caller OWNS, while spawn_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 via list_sessions."

Fixed by a sibling dep, listSharedWorkspaces (session-tools-runtime.ts): other members' sessions in drives the caller belongs to, reported as a distinct sharedWorkspaces section of the list_sessions output so the model always knows which workspaces are its own and which are shared.

  • Same predicate, never a second one. Per-row access is decided by the exact pure decision the spawn gate applies on the explicit-workspaceId path (checkSessionAccessdecideAgentSessionAccess, fed by the same resolveDriveMembership gather — checkAccessForSubject's composition). Candidate drives come from getDriveIdsForUser, 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.
  • Cap semantics, made explicit. The caller's OWN set keeps leaning on the structural MAX_ACTIVE_WORKSPACES_PER_OWNER spawn 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.
  • Deliberate description change, not drift. list_sessions' tool description now names the sharedWorkspaces section, 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 in session-tools-schema.test.ts is 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:

  • The workspace's owner sees everything in their own workspace, unchanged.
  • Any other viewer sees a title only for threads that are their own or deliberately shared (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).
  • Transcript content stays owner-gated regardless (openOwnSession — unchanged).

Implemented in the listing mapping layer in one place: the pure redactConversationTitleForViewer (fail-closed), routed through both viewer-facing mappings (listWorkspaceWorkers for the caller's current workspace — which can be a shared one they were spawned into — and listSharedWorkspaces). The rule is documented where the queries live (listSessionConversationsBulk's doc, per the issue's own ask), and SessionConversationEntry now carries ownerId/isShared so 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.tslistSharedWorkspaces runs 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 — the sharedWorkspaces section 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.ts updated 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.
  • Session-tools + agent-sessions suites (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

…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
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 460d4508-5293-407e-98eb-36bd2c81dfc7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@2witstudios
2witstudios merged commit ffbea38 into pu/broken-sessions Aug 6, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant