Skip to content

refactor(agents): kill the sessionId overload, collapse singletons, pin the tool contract - #2340

Merged
2witstudios merged 3 commits into
pu/broken-sessionsfrom
refactor/session-naming-singletons
Aug 6, 2026
Merged

refactor(agents): kill the sessionId overload, collapse singletons, pin the tool contract#2340
2witstudios merged 3 commits into
pu/broken-sessionsfrom
refactor/session-naming-singletons

Conversation

@2witstudios

Copy link
Copy Markdown
Owner

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 the sessionId overload in the tool layer (behavior-neutral)

  • SessionToolRowWorkerRow {conversationId, ownerId, agentPageId, name, workspaceId, isClosed} — the tool family's sessionId field was a conversation id and workspaceSessionId was the agent_sessions.id; the internals now use the spec §4 vocabulary. The hardcoded-null endedAt field is deleted (dead weight; no consumer branched on it).
  • Deps renamed to match: findSessionfindWorker(conversationId), listSessionWorkerslistWorkspaceWorkers, countSessionConversationscountOpenConversations, endSessionkillWorker, plus every dep input that carried a conversation id under the name sessionId.
  • Frozen wire contract: tool names, zod parameter names, descriptions, and model-facing JSON shapes are byte-identical. Each tool body maps input.sessionId → a conversationId local on its first line; WorkerListingEntry.sessionId stays (it serializes into list_sessions output). New session-tools-schema.test.ts pins every tool's name + description + JSON input schema as explicit literals (not .snap files — an accidental --update can't re-pin them), so any future wire drift fails CI.
  • Replaced the stale session-tools.ts module 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)

  • One 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. executor defaults to db so a lock-holding caller can pass its own connection.
  • One MAX_ACTIVE_WORKSPACES_PER_OWNER = 100 in the packages/lib contract module; SESSION_LIST_LIMIT deleted (the store's .limit() imports the contract constant); apps/web's MAX_ACTIVE_SESSIONS_PER_OWNER is now a re-export. The listOwnWorkspaces post-cap-exclusion soundness is structural (one constant), and the invariant test from 4c0c96d61 now pins the re-export identity.
  • The 'about-to-be-minted' sentinel dies: decideAgentSessionAccess never reads the subject's id (verified — it keys on ownerId + driveId only), so AgentSessionAccessSubject drops sessionId and the pre-mint callers (agent-sessions POST route ×2, worker-placement paths ×2) stop fabricating it. sessionQuotaExceeded's audit param becomes workspaceId: string | null — a pre-mint quota refusal now logs an honestly absent resourceId instead of the literal sentinel string.

3. 486b83fa3 — tool contract pin + typed refusals (the deliberate behavior change)

  • Per spec §2: resources the caller does not own (no row, or foreign owner) still read as nonexistent with the byte-identical message — anti-enumeration preserved and pinned by a foreign-vs-missing equality test. The caller's own rows now get two distinct typed refusals: unbound thread → 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 in session-tools-runtime.test.ts (killWorker aborts streams only; never calls workspace endSession).
  • Spec doc updated in the same PR per §5's own rule: §2's typed-refusal "Phase 1 target" flipped to shipped; §5's "forthcoming" contract-test reference now names the real test files.

Behavior changes

Only the two new owned-row refusal messages (+ reason fields) from commit 3, and the audit rows that used to carry 'about-to-be-minted' as a resourceId now 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 is src/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 under TZ=UTC (verified both ways); nothing in this PR touches message grouping.
  • Targeted suites: all session-tools tests (schema, contract, pure, runtime, dispatch, origin, invariant), packages/lib agent-sessions suites (14 files / 365 tests), web agent-sessions suites incl. integration, realtime shell-access — pass

🤖 Generated with Claude Code

https://claude.ai/code/session_01XnzUuAixdTJ8xDpP5S92Ts

2witstudios and others added 3 commits August 5, 2026 20:12
…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
@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: e097ea31-44bf-48ad-839f-524091da655d

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 ece69af into pu/broken-sessions Aug 6, 2026
10 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