refactor(agents): kill the sessionId overload, collapse singletons, pin the tool contract - #2340
Conversation
…rsationId/workspaceId internals
SessionToolRow becomes WorkerRow {conversationId, ownerId, agentPageId,
name, workspaceId, isClosed}: the tool family's "sessionId" IS a worker's
conversation id and its "workspaceSessionId" IS the agent_sessions.id, so
the internals now say what they mean (spec §4). The hardcoded-null endedAt
field is deleted outright — a worker conversation never ends, and no
consumer ever branched on it. Deps follow: findSession→findWorker,
listSessionWorkers→listWorkspaceWorkers, countSessionConversations→
countOpenConversations, endSession→killWorker, and every dep input that
carried a conversation id under the name sessionId now says conversationId.
The WIRE CONTRACT is frozen and pinned: tool names, zod parameter names,
descriptions, and the JSON shapes returned to the model are byte-identical
(WorkerListingEntry.sessionId stays — it serializes into list_sessions
output). Each tool body maps input.sessionId to a conversationId local on
its first line, and a new explicit-literal contract test
(session-tools-schema.test.ts) pins every tool's name, description and
JSON input schema so any future wire drift fails CI.
Also replaces the stale module header in session-tools.ts, which still
documented the cross-workspace confinement guard PR #2336 deleted, with a
pointer to the resource-addressed model and the spec doc
(docs/2.0-architecture/agent-sessions.md §2/§4) — the exact tool-guidance
drift class §5 exists to stop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XnzUuAixdTJ8xDpP5S92Ts
…-sentinel singletons Three facts each stored twice-or-more become one owner each (epic Phase 1, D7; #2161's derive-don't-copy rule): - ONE countOpenConversations(workspaceId, executor?) in the new conversation-cap.ts, carrying the single open-listing predicate (sessionId = $1 AND isActive AND closedInSessionAt IS NULL). The three hand-written copies — sessionListingReadDeps' claim/close/reopen count, countOpenConversationsForSession, and the tool layer's spawn pre-count — are now thin calls, so the cap's symmetry across create/close/reopen/spawn is structural instead of mirrored comments. The executor parameter lets a lock-holding caller pass its own connection. - ONE MAX_ACTIVE_WORKSPACES_PER_OWNER = 100 in the packages/lib contract module. SESSION_LIST_LIMIT is deleted (the store's .limit() reads the contract constant) and apps/web's MAX_ACTIVE_SESSIONS_PER_OWNER becomes a re-export — the "listing never truncates an owner's real set" soundness listOwnWorkspaces leans on is now one number, not two constants in two packages pinned equal by a test. The invariant test (4c0c96d) now pins the re-export identity instead. - The 'about-to-be-minted' sentinel dies. decideAgentSessionAccess never read the subject's id (it keys on ownerId + driveId only), so AgentSessionAccessSubject drops sessionId and pre-mint callers (agent-sessions POST route, worker-placement in session-tools-runtime) stop fabricating a fake id into a typed interface. sessionQuotaExceeded's audit param becomes workspaceId: string | null — a pre-mint quota refusal logs an honestly absent resourceId, never a sentinel. Behavior-neutral except the audit rows that used to carry the literal string 'about-to-be-minted' as a resourceId. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XnzUuAixdTJ8xDpP5S92Ts
…c flipped to shipped The deliberate Phase 1 behavior change (spec §2, axiom 1): openOwnSession stops collapsing four failure causes into one not-yours message. Resources the caller does NOT own (no row, or owner mismatch) still read as nonexistent with the byte-identical message — anti-enumeration preserved, pinned by a foreign-vs-missing equality test. The caller's OWN rows now get distinct, typed, actionable refusals: an unbound thread answers reason: 'not_a_worker' with running-spawn_session-from-inside-it guidance, and a human-closed listing answers reason: 'worker_closed' with the reopen-or-spawn-fresh remedy. One cause, one message, mapped at the tool boundary. Contract tests (session-tools-contract.test.ts, same DI style as session-tools.test.ts) pin every behavioral claim each tool description makes to the gate that enforces it — list_sessions' from-anywhere addressability, spawn_session's three workspace placement modes and fire-and-forget default, send/read_session's wait and untrusted-framing claims, kill_session's worker-only kill, spawn/send/read/kill_shell's own-workspace scoping and already-gone-succeeds — plus the refusal contract itself. kill_session's "never tears the sandbox down" is pinned against the production wiring in session-tools-runtime.test.ts: killWorker aborts the worker's streams only and never calls the workspace-lifecycle endSession. Per §5's own rule, the spec doc updates in the same PR: §2's Phase 1 typed-refusal target is flipped to shipped, and §5's "forthcoming" contract-test reference now names the real test files. 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 1 of the Agent-Session Single Source of Truth epic (spec:
docs/2.0-architecture/agent-sessions.md, D7 of the epic plan). Three logical changes, one commit each:1.
eaf649662— kill thesessionIdoverload in the tool layer (behavior-neutral)SessionToolRow→WorkerRow {conversationId, ownerId, agentPageId, name, workspaceId, isClosed}— the tool family'ssessionIdfield was a conversation id andworkspaceSessionIdwas theagent_sessions.id; the internals now use the spec §4 vocabulary. The hardcoded-nullendedAtfield is deleted (dead weight; no consumer branched on it).findSession→findWorker(conversationId),listSessionWorkers→listWorkspaceWorkers,countSessionConversations→countOpenConversations,endSession→killWorker, plus every dep input that carried a conversation id under the namesessionId.input.sessionId→ aconversationIdlocal on its first line;WorkerListingEntry.sessionIdstays (it serializes intolist_sessionsoutput). Newsession-tools-schema.test.tspins every tool's name + description + JSON input schema as explicit literals (not.snapfiles — an accidental--updatecan't re-pin them), so any future wire drift fails CI.session-tools.tsmodule header that still documented the cross-workspace guard PR feat(agents): fully resource-addressed session orchestration — permission-gated, any surface, any workspace (#2335) #2336 deleted, with a pointer to the resource-addressed model + the spec doc.2.
5fb367f01— singleton collapse (behavior-neutral except honest audit rows)countOpenConversations(workspaceId, executor?)(apps/web/src/lib/agent-sessions/conversation-cap.ts) carrying the single open-listing predicate (sessionId = $1 AND isActive AND closedInSessionAt IS NULL); the three duplicate query sites (claim/close/reopen read-deps,countOpenConversationsForSession, the tool layer's spawn pre-count) become thin calls.executordefaults todbso a lock-holding caller can pass its own connection.MAX_ACTIVE_WORKSPACES_PER_OWNER = 100in the packages/lib contract module;SESSION_LIST_LIMITdeleted (the store's.limit()imports the contract constant); apps/web'sMAX_ACTIVE_SESSIONS_PER_OWNERis now a re-export. ThelistOwnWorkspacespost-cap-exclusion soundness is structural (one constant), and the invariant test from4c0c96d61now pins the re-export identity.'about-to-be-minted'sentinel dies:decideAgentSessionAccessnever reads the subject's id (verified — it keys onownerId+driveIdonly), soAgentSessionAccessSubjectdropssessionIdand the pre-mint callers (agent-sessions POST route ×2, worker-placement paths ×2) stop fabricating it.sessionQuotaExceeded's audit param becomesworkspaceId: string | null— a pre-mint quota refusal now logs an honestly absentresourceIdinstead of the literal sentinel string.3.
486b83fa3— tool contract pin + typed refusals (the deliberate behavior change)reason: 'not_a_worker'with spawn-from-inside-it guidance; human-closed listing →reason: 'worker_closed'with the reopen-or-spawn-fresh remedy.session-tools-contract.test.ts: every behavioral claim in every tool description pinned to the gate that enforces it (list_sessions from-anywhere addressability, spawn_session's three workspace modes + fire-and-forget default, untrusted transcript framing, shell scoping, already-gone-succeeds, the full refusal matrix). kill_session's "never tears the sandbox down" is pinned against the production wiring insession-tools-runtime.test.ts(killWorkeraborts streams only; never calls workspaceendSession).Behavior changes
Only the two new owned-row refusal messages (+
reasonfields) from commit 3, and the audit rows that used to carry'about-to-be-minted'as aresourceIdnow omitting it. Everything model-facing is otherwise pinned byte-identical by the schema snapshot test.Gates (run sequentially, real results)
bun run lint— pass (14/14 tasks)bun run knip:check— pass (4 issues, all within baseline 4)bun run typecheck— pass (16/16 tasks)bun run test:unit(full @pagespace/lib + web suites, against the shared 5433 test Postgres) — 16160 passed, 1 failed, 6 skipped. The one failure issrc/lib/messages/__tests__/grouping.test.ts("breaks the group when messages cross midnight"), the documented TZ-dependent env-only failure: it fails identically in this machine's local TZ with or without this branch and passes underTZ=UTC(verified both ways); nothing in this PR touches message grouping.packages/libagent-sessions suites (14 files / 365 tests), web agent-sessions suites incl. integration, realtimeshell-access— pass🤖 Generated with Claude Code
https://claude.ai/code/session_01XnzUuAixdTJ8xDpP5S92Ts