Skip to content

feat: route agent sessions by conversation#1089

Open
Nikhil (shadowfax92) wants to merge 6 commits into
mainfrom
fix/0601-1647-walrus
Open

feat: route agent sessions by conversation#1089
Nikhil (shadowfax92) wants to merge 6 commits into
mainfrom
fix/0601-1647-walrus

Conversation

@shadowfax92
Copy link
Copy Markdown
Contributor

@shadowfax92 Nikhil (shadowfax92) commented Jun 2, 2026

Summary

  • Reworked New Tab agent UX so there is no product-level main conversation default.
  • Added canonical full-page agent routes at /home/agents/:agentId/sessions/:sessionId; old /home/agents/:agentId redirects to the latest session or a fresh UUID.
  • Home recent-agent cards now resume latestSessionId, while the Home composer starts ACP sends in a fresh UUID session.
  • Added a full-page chat header plus button to start a new conversation for the same agent.
  • Added session-specific server chat/active/stream/cancel/queue routes while keeping old main routes as compatibility fallbacks.
  • Queued messages now retain sessionId and drain back into that session.

Rework

The previous approach forced New Tab into main, which solved leakage but created the wrong UX. This rework treats every full-page agent chat as session-addressed: resume the latest real session by default, and use the plus button when the user wants a new session.

Test plan

  • bun run --filter @browseros/server test:api
  • bun run --filter @browseros/server test:lib
  • bun run --filter @browseros/agent test
  • bun run --filter @browseros/server typecheck
  • bun run --filter @browseros/agent typecheck
  • git diff --name-only | sed 's#^packages/browseros-agent/##' | xargs bunx biome check
  • git diff --check

@shadowfax92
Copy link
Copy Markdown
Contributor Author

Greptile (@greptileai) review

@shadowfax92
Copy link
Copy Markdown
Contributor Author

Claude (@claude) review

@claude
Copy link
Copy Markdown

claude Bot commented Jun 2, 2026

Claude finished Nikhil (@shadowfax92)'s task in 3m 12s —— View job


Code Review

  • Gathering PR context
  • Reading changed files
  • Analyzing implementation
  • Posting review feedback

This PR routes agent sessions per-conversation by threading an agentSessionId (UUID or "main") through the full stack — from sidepanel request builder → API route → harness service → registry → ACPX runtime context/state. The implementation is solid and well-tested. Below are my findings.


Architecture

The two-level state file design is the right call:

  • runtimeSessionStatePath — per-session pointer (harness/runtime-state/<agentId>/<encodedSessionId>.json)
  • runtimeStatePath — agent-level latest pointer (harness/runtime-state/<agentId>.json)

loadSessionRecord correctly falls back: session-level → agent-level (for main only) → legacy agent.sessionKey. This preserves backward compatibility for existing agents.


Issues

1. listSessions still hardcodes [{ id: 'main' }]

AcpxRuntime.listSessions() (acpx-runtime.ts:99–109) always returns only the main session regardless of how many conversation sessions actually exist. If any UI path calls listSessions to enumerate sessions, it will silently omit non-main sessions. If this method is unused for session enumeration today, it's worth a comment; otherwise it needs to enumerate real sessions from the filesystem.

2. Redundant agentSessionId fallback in request builder

In useChatSessionRequest.ts:38:

agentSessionId: agentSessionId ?? common.conversationId,

agentSessionId is always defined at the call site — useChatSession.ts unconditionally resolves it to either 'main' or conversationIdRef.current before calling the request builder. The ?? fallback is dead code and misleads readers into thinking agentSessionId might be undefined. Either type the param as required, or document why the fallback exists.

3. Per-session state files are never purged

finishBrowserosManagedContext writes a new runtimeSessionStatePath file for every unique (agentId, sessionId) pair. In a long-running BrowserOS instance with many sidepanel conversations, these small JSON files accumulate indefinitely. The agent-level sweeper in TurnRegistry cleans in-memory turns but nothing touches the filesystem. This is likely acceptable short-term but worth a follow-up issue to cap / TTL these files.

4. agentSessionStrategy default when origin is absent

useChatSession.ts:356–358:

const agentSessionStrategy =
  options?.agentSessionStrategy ??
  (options?.origin === 'newtab' ? 'main' : 'conversation')

When origin is undefined, strategy defaults to 'conversation', which routes by conversation UUID. In practice origin is always explicit today, but if a future caller omits it, it silently gets conversation routing. A comment or a narrower default (e.g., log a warning) would help.


Minor Observations

  • saveLatestRuntimeState writes both paths with the same latest (including the sidepanel's sessionId) — so the agent-level state file always reflects the most recently active session. This is intentional for getRowSnapshot, but it means the agent-level file's sessionId field can be a non-main UUID after sidepanel activity. Fine as long as callers only use that file as a pointer to runtimeSessionKey.

  • parseSidepanelAgentChatBody fallback (agents.ts:693–697) is a good backward-compat backstop: old clients that don't send agentSessionId get conversationId routing automatically.

  • hasActiveForAgent in the activity update (agent-harness-service.ts:319–327) correctly handles concurrent sessions: it keeps 'working' status if any session is still running after one finishes. The fix from deleting immediately to checking was necessary.

  • RingBuffer terminal frame preservation (active-turn-registry.ts) in slice() re-attaches the terminal even after eviction as long as terminal.seq > fromSeq. Edge case: if a subscriber reconnects with fromSeq >= terminal.seq, it won't receive a terminal. This seems benign in practice (you'd only reconnect with a seq you haven't seen), but worth a comment on the slice method.

  • isAgentSessionId validation in the API route is clean. Accepting "main" or UUID-v4 is the right shape. Emitting a 400 with a clear message is correct.


Test Coverage

Test additions are extensive: acpx-runtime.test.ts (+146 lines), agent-harness-service.test.ts (+165), agents.test.ts (+189), plus acpx-runtime-context, acpx-runtime-state, and active-turn-registry tests. The concurrent-session collision scenario (agents.test.ts:620–661) and the per-session history routing (agent-harness-service.test.ts:157–192) are the most important new paths and both appear to be covered.


Summary

The core session-routing logic is correct and the test suite is thorough. The most actionable issue before merging is #1 (auditing whether listSessions needs to enumerate non-main sessions). Issues #2#4 are low-risk cleanup items.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 2, 2026

Greptile Summary

This PR replaces the hardcoded 'main'-session model with a fully session-addressed full-page agent chat UX. Every canonical page route is now /home/agents/:agentId/sessions/:sessionId; the old session-less route redirects to the agent's latestSessionId or a fresh UUID. Old main-session server routes are preserved as compatibility fallbacks.

  • Server: New per-session routes for chat, stream, active, cancel, queue, and history; AgentHarnessService activity map rekeyed to agentId\0sessionId; AcpxRuntime adds getLatestRowSnapshot + a session-scoped runtimeSessionStatePath alongside the existing agent-level pointer file; QueuedMessage now carries an optional sessionId so queued messages drain back into the originating session.
  • Client: AgentCommandConversation reads :sessionId from URL params, redirects on first load, filters the display queue by session, and adds a "New conversation" plus button; all agent API helpers accept an optional sessionId and route to the appropriate endpoint via buildSessionChatPath.
  • Sidepanel: useChatSession gains an agentSessionStrategy option ('conversation' | 'main') so existing sidepanel flows can be opted into main-session semantics while new full-page sessions use the conversation UUID.

Confidence Score: 5/5

Safe to merge. The session-routing changes are well-encapsulated, backward-compatible routes are in place, and the test suite covers the new session-scoped paths, history loading, runtime state, and queue behaviour.

The implementation is thorough: runtime validation guards all session-ID API inputs, the persisted state uses two pointer files (session-scoped and agent-scoped) for correct latestSessionId resolution and backward compatibility, and in-memory activity is cleanly keyed per session. The one behavioural gap — cross-session head-of-line blocking in the shared FIFO queue when multiple sessions have queued messages — causes delayed (not lost) messages and requires a specific multi-tab concurrent-queue scenario to surface.

The queue drain logic in agent-harness-service.ts (maybeStartNextFromQueue) warrants a second look if multi-session concurrent queuing becomes a common user flow.

Important Files Changed

Filename Overview
packages/browseros-agent/apps/server/src/api/routes/agents.ts Added session-specific routes (chat, stream, cancel, queue, history) alongside legacy main compatibility routes; extracted startChatTurnResponse and streamExistingTurn helpers.
packages/browseros-agent/apps/server/src/api/services/agents/agent-harness-service.ts Activity map rekeyed to agentId\0sessionId; queue drain checks per-session active turns but pops the globally oldest message, which can delay unblocked sessions.
packages/browseros-agent/apps/server/src/lib/agents/acpx-runtime.ts Added getLatestRowSnapshot and loadLatestAgentSessionRecord; loadSessionRecord reads session-specific state file with main-session fallback to the agent-level pointer.
packages/browseros-agent/apps/server/src/lib/agents/message-queue.ts Added optional sessionId to QueuedMessage and append input so queued messages drain back into the originating session.
packages/browseros-agent/apps/server/src/lib/agents/acpx-runtime-context.ts Added runtimeSessionStatePath (session-scoped pointer file) alongside the existing agent-level runtimeStatePath.
packages/browseros-agent/apps/agent/entrypoints/app/agent-command/AgentCommandConversation.tsx Reads :sessionId from URL params; redirects to latestSessionId or fresh UUID when absent; adds per-session queue filtering and a New conversation plus button.
packages/browseros-agent/apps/agent/entrypoints/app/agents/useAgents.ts All agent API helpers now accept sessionId; buildSessionChatPath selects the legacy or session-scoped URL.
packages/browseros-agent/apps/agent/entrypoints/app/agent-command/pending-initial-message.ts Added sessionId to PendingInitialMessage; consumePendingInitialMessage now requires both agentId and sessionId to match.
packages/browseros-agent/apps/agent/entrypoints/sidepanel/index/useChatSession.ts Added AgentSessionStrategy option; sidepanel uses conversation-id as agentSessionId by default.
packages/browseros-agent/apps/server/src/lib/agents/active-turn-registry.ts Session type widened from 'main' literal to AgentSessionId; register and getActiveFor now accept any session ID.
packages/browseros-agent/apps/server/src/lib/agents/agent-types.ts Introduced MAIN_AGENT_SESSION_ID constant and AgentSessionId = string type alias; widened sessionId fields across history/session types.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/browseros-agent/apps/server/src/api/services/agents/agent-harness-service.ts:413-430
**Cross-session queue drain head-of-line blocking**

`maybeStartNextFromQueue` always pops the globally oldest message regardless of its session. If that message belongs to session A (which currently has an active turn), it is pushed back to the front and the drain returns — leaving session B's queued message stranded even though session B has no active turn. Session B's message will only be drained after session A's current turn finishes.

In the multi-tab case (tab 1 on agent/session-A, tab 2 on agent/session-B), a user on tab 2 who types a message while agent/session-A has an outstanding queued message in front of it in the FIFO will see no response until session A drains. The message is not lost, but the delay is invisible to the user.

The fix would require either a per-session sub-queue or a scan-for-next-startable-session strategy in `maybeStartNextFromQueue`.

### Issue 2 of 2
packages/browseros-agent/apps/server/src/lib/agents/agent-types.ts:10-11
**`AgentSessionId` is an unbranded string alias**

`type AgentSessionId = string` is structurally identical to `string`, so TypeScript will accept any `string` wherever an `AgentSessionId` is expected and vice versa. Runtime validation via `isAgentSessionId` catches bad values at the API boundary, but internal call sites get no compile-time protection — a raw `string` variable could silently flow into a session-scoped registry key or file-system path without the type system objecting. A nominal/branded type (`type AgentSessionId = string & { readonly __brand: 'AgentSessionId' }`) would surface these mistakes at compile time.

Reviews (3): Last reviewed commit: "refactor: rework agent-session-routing b..." | Re-trigger Greptile

Comment on lines 671 to 676
input: {
sessionId?: AgentSessionId
message: string
attachments?: ReadonlyArray<{ mediaType: string; data: string }>
cwd?: string
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused sessionId field in runDetachedTurn input type

input.sessionId is declared in the input type signature but is never read inside the function body — the implementation immediately shadows it with const sessionId = turn.sessionId. Since the turn was already registered with the correct session ID in startTurn, deriving it from the registry is correct, but the sessionId?: AgentSessionId field in the parameter type becomes dead code that adds noise and could mislead future callers into thinking it influences behaviour.

Rule Used: Remove unused/dead code rather than leaving it in ... (source)

Learned From
browseros-ai/BrowserOS-agent#126

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/api/services/agents/agent-harness-service.ts
Line: 671-676

Comment:
**Unused `sessionId` field in `runDetachedTurn` input type**

`input.sessionId` is declared in the input type signature but is never read inside the function body — the implementation immediately shadows it with `const sessionId = turn.sessionId`. Since the turn was already registered with the correct session ID in `startTurn`, deriving it from the registry is correct, but the `sessionId?: AgentSessionId` field in the parameter type becomes dead code that adds noise and could mislead future callers into thinking it influences behaviour.

**Rule Used:** Remove unused/dead code rather than leaving it in ... ([source](https://app.greptile.com/browseros-org-2/-/custom-context?memory=9b045db4-2630-428c-95b7-ccf048d34547))

**Learned From**
[browseros-ai/BrowserOS-agent#126](https://github.com/browseros-ai/BrowserOS-agent/pull/126)

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +222 to +237
private async loadLatestAgentSessionRecord(
agent: AgentPromptInput['agent'],
): Promise<AcpSessionRecord | null> {
const paths = resolveAgentRuntimePaths({
browserosDir: this.browserosDir,
agentId: agent.id,
})
const latestForAgent = await loadLatestRuntimeState(paths.runtimeStatePath)
if (latestForAgent) {
const latestRecord = await this.sessionStore.load(
latestForAgent.runtimeSessionKey,
)
if (latestRecord) return latestRecord
}
return this.loadSessionRecord(agent, MAIN_AGENT_SESSION_ID)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant runtimeStatePath reads in loadLatestAgentSessionRecord

When latestForAgent is found in runtimeStatePath but the session store doesn't have the corresponding runtimeSessionKey record (e.g. the store was cleaned up while the pointer file remained), execution falls through to loadSessionRecord(agent, MAIN_AGENT_SESSION_ID). That function also reads paths.runtimeStatePath for the main session fallback path, causing the same file to be read twice. Consider passing the already-loaded latestForAgent value into loadSessionRecord (or inlining the final fallback) to avoid the duplicate I/O.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/src/lib/agents/acpx-runtime.ts
Line: 222-237

Comment:
**Redundant `runtimeStatePath` reads in `loadLatestAgentSessionRecord`**

When `latestForAgent` is found in `runtimeStatePath` but the session store doesn't have the corresponding `runtimeSessionKey` record (e.g. the store was cleaned up while the pointer file remained), execution falls through to `loadSessionRecord(agent, MAIN_AGENT_SESSION_ID)`. That function also reads `paths.runtimeStatePath` for the `main` session fallback path, causing the same file to be read twice. Consider passing the already-loaded `latestForAgent` value into `loadSessionRecord` (or inlining the final fallback) to avoid the duplicate I/O.

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 2, 2026

Greptile Summary

This PR introduces first-class, conversation-scoped agent sessions by threading an agentSessionId (either 'main' or a conversation UUID) through the entire stack: sidepanel request builder → server route → AgentHarnessServiceTurnRegistryAcpxRuntime. History and runtime-state storage gains a per-session file (runtime-state/{agentId}/{sessionId}.json) alongside the existing agent-level pointer, with a backward-compatible fallback chain for 'main' that keeps pre-PR session history accessible.

  • Session routing: The sidepanel now defaults to agentSessionStrategy = 'conversation' (routes by conversationId); new-tab and generic chat keep 'main'. The isAgentSessionId guard at the API boundary rejects anything that is not 'main' or a UUID.
  • Concurrency: TurnRegistry already differentiates sessions within the same agent, and a new hasActiveForAgent helper correctly keeps the agent in 'working' state until all sessions finish.
  • Backward compatibility: loadSessionRecord falls back through session file → agent-level file (only if sessionId === 'main') → agent.sessionKey, so existing agents without the new session files still serve their history correctly.

Confidence Score: 4/5

Safe to merge; the session-routing logic is well-covered by tests and the backward-compatibility fallback chain for existing main-session history is correctly gated.

The implementation is coherent end-to-end and the test suite is thorough. The two comments are minor: AgentSessionId = string offers no compile-time narrowing against arbitrary strings, and a 5 ms setTimeout in the concurrency test is a potential source of intermittent CI failures rather than a functional defect.

The concurrency test in tests/api/routes/agents.test.ts (timing-based setTimeout) and the AgentSessionId type definition in agent-types.ts are the only places worth a second look.

Important Files Changed

Filename Overview
packages/browseros-agent/apps/server/src/lib/agents/agent-types.ts Introduces AgentSessionId = string type alias and MAIN_AGENT_SESSION_ID constant; the alias is intentionally broad (any string satisfies it), with validation delegated to isAgentSessionId at API boundaries only.
packages/browseros-agent/apps/server/src/lib/agents/active-turn-registry.ts Adds hasActiveForAgent and widens sessionId from 'main' literal to AgentSessionId; per-session turn isolation is correctly implemented in register and getActiveFor.
packages/browseros-agent/apps/server/src/lib/agents/acpx-runtime.ts Splits loadLatestSessionRecord into loadSessionRecord (session-specific with backward-compat fallback chain for main) and loadLatestAgentSessionRecord (agent-level row snapshot); logic is sound but the three-level fallback for main adds complexity.
packages/browseros-agent/apps/server/src/lib/agents/acpx-runtime-context.ts Adds runtimeSessionStatePath field to AgentRuntimePaths; uses encodeURIComponent for the session filename (safe for both 'main' and UUID values).
packages/browseros-agent/apps/server/src/lib/agents/acpx-agent-common.ts Writes session state to both runtimeSessionStatePath (session-level) and runtimeStatePath (agent-level) in parallel; atomic renames make each write safe, though the agent-level pointer is a last-writer-wins race under concurrency (accepted design).
packages/browseros-agent/apps/server/src/api/services/agents/agent-harness-service.ts Propagates sessionId through startTurn, cancelTurn, getHistory, and send; correctly derives sessionId from the registered turn in runDetachedTurn; activity tracking updated to consider all sessions via hasActiveForAgent.
packages/browseros-agent/apps/server/src/api/routes/agents.ts Widens /:agentId/sessions/main/history to /:agentId/sessions/:sessionId/history with isAgentSessionId validation; adds agentSessionId to sidepanel chat request with fallback to conversationId for backward compatibility.
packages/browseros-agent/apps/agent/entrypoints/sidepanel/index/useChatSession.ts Introduces AgentSessionStrategy and derives agentSessionId from strategy; defaults sidepanel to 'conversation' (uses conversationId) and newtab to 'main'.
packages/browseros-agent/apps/server/tests/api/routes/agents.test.ts New tests verify per-session history, concurrent sessions, explicit main-session override, and agentSessionId validation rejection; concurrent test uses a 5 ms setTimeout for synchronization which may be fragile under load.

Sequence Diagram

sequenceDiagram
    participant SP as Sidepanel (useChatSession)
    participant Req as useChatSessionRequest
    participant Route as /agents/:id/sidepanel/chat
    participant Svc as AgentHarnessService
    participant Reg as TurnRegistry
    participant RT as AcpxRuntime

    SP->>SP: "agentSessionStrategy = 'conversation' (sidepanel) or 'main' (newtab)"
    SP->>SP: "agentSessionId = strategy==='main' ? 'main' : conversationId"
    SP->>Req: "buildSidepanelPreparedSendMessagesRequest({ agentSessionId, ... })"
    Req->>Route: "POST body { conversationId, agentSessionId, message }"

    Route->>Route: parseSidepanelAgentChatBody() validates isAgentSessionId(agentSessionId)
    Route->>Svc: "startTurn({ agentId, sessionId: agentSessionId, message })"
    Svc->>Reg: register(agentId, sessionId)
    Reg-->>Svc: "ActiveTurn { turnId, sessionId }"

    Route->>Route: "response header X-Session-Id = agentSessionId"

    Svc->>RT: "send({ agent, sessionId, ... })"
    RT->>RT: "resolveAgentRuntimePaths({ agentId, sessionId })"
    RT->>RT: write runtimeSessionStatePath (session-specific)
    RT->>RT: write runtimeStatePath (agent-level pointer)

    Note over Svc,Reg: On turn complete
    Svc->>Reg: hasActiveForAgent(agentId)?
    alt other sessions still running
        Svc->>Svc: activity.set(agentId, 'working')
    else no active sessions
        Svc->>Svc: activity.delete(agentId)
    end

    Note over Route,RT: History lookup path
    Route->>Svc: getHistory(agentId, sessionId)
    Svc->>RT: loadSessionRecord(agent, sessionId)
    RT->>RT: try runtimeSessionStatePath then sessionStore
    RT->>RT: if main: fallback runtimeStatePath then sessionStore
    RT->>RT: if main: fallback agent.sessionKey
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/browseros-agent/apps/server/src/lib/agents/agent-types.ts:11-12
The `AgentSessionId` type alias is identical to `string`, so any arbitrary string satisfies it at compile time. The runtime guard `isAgentSessionId` is only applied at the API boundary in `agents.ts`; internal callers (e.g., passing a raw string instead of `MAIN_AGENT_SESSION_ID`) would silently type-check. A discriminated union or branded type would surface misuse earlier.

```suggestion
export const MAIN_AGENT_SESSION_ID = 'main'
/** Either the sentinel `'main'` value or a UUID string. Validated at API boundaries via `isAgentSessionId`. */
export type AgentSessionId = typeof MAIN_AGENT_SESSION_ID | (string & {})
```

### Issue 2 of 2
packages/browseros-agent/apps/server/tests/api/routes/agents.test.ts:615-623
A fixed 5 ms delay is used to ensure the first request is in-flight before the second is issued. This can be flaky in slow CI environments where Node event-loop turns take longer than expected. A deterministic synchronization point (e.g., waiting for the blocking service to acknowledge it received the first turn) would make this reliable.

```suggestion
    const first = route.request('/agents/agent-1/sidepanel/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        ...validCreatedAgentSidepanelBody(),
        conversationId: firstSessionId,
      }),
    })
    // Wait until the blocking service has registered the first turn before
    // issuing the second request, so the registry state is deterministic.
    await blocking._firstTurnStarted
```

Reviews (2): Last reviewed commit: "feat: send agent session ids from chat c..." | Re-trigger Greptile

Comment on lines +615 to +623
const first = route.request('/agents/agent-1/sidepanel/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...validCreatedAgentSidepanelBody(),
conversationId: firstSessionId,
}),
})
await new Promise((r) => setTimeout(r, 5))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 A fixed 5 ms delay is used to ensure the first request is in-flight before the second is issued. This can be flaky in slow CI environments where Node event-loop turns take longer than expected. A deterministic synchronization point (e.g., waiting for the blocking service to acknowledge it received the first turn) would make this reliable.

Suggested change
const first = route.request('/agents/agent-1/sidepanel/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...validCreatedAgentSidepanelBody(),
conversationId: firstSessionId,
}),
})
await new Promise((r) => setTimeout(r, 5))
const first = route.request('/agents/agent-1/sidepanel/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...validCreatedAgentSidepanelBody(),
conversationId: firstSessionId,
}),
})
// Wait until the blocking service has registered the first turn before
// issuing the second request, so the registry state is deterministic.
await blocking._firstTurnStarted
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browseros-agent/apps/server/tests/api/routes/agents.test.ts
Line: 615-623

Comment:
A fixed 5 ms delay is used to ensure the first request is in-flight before the second is issued. This can be flaky in slow CI environments where Node event-loop turns take longer than expected. A deterministic synchronization point (e.g., waiting for the blocking service to acknowledge it received the first turn) would make this reliable.

```suggestion
    const first = route.request('/agents/agent-1/sidepanel/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        ...validCreatedAgentSidepanelBody(),
        conversationId: firstSessionId,
      }),
    })
    // Wait until the blocking service has registered the first turn before
    // issuing the second request, so the registry state is deterministic.
    await blocking._firstTurnStarted
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@shadowfax92
Copy link
Copy Markdown
Contributor Author

Updated: sidepanel UUID sessions no longer affect the New Tab main-session recent-agent row, status, preview, or error badge. Added regression coverage for sidepanel errors and requested-session snapshots.

@shadowfax92
Copy link
Copy Markdown
Contributor Author

Greptile (@greptileai) review

@shadowfax92
Copy link
Copy Markdown
Contributor Author

Claude (@claude) review

@claude
Copy link
Copy Markdown

claude Bot commented Jun 2, 2026

Claude finished Nikhil (@shadowfax92)'s task in 4m 42s —— View job


Code Review

  • Gathering PR context
  • Reading changed files
  • Reviewing new commits since last review
  • Analyzing implementation
  • Posting review feedback

This review covers all 6 commits, with particular focus on the two new commits (c5fa522 "fix: isolate sidepanel agent activity from home" and fd4d59e "refactor: rework agent-session-routing based on feedback") added after the previous round of feedback.

What's Working Well

The per-session activity isolation in c5fa522 is well-designed. Using activityKey(agentId, sessionId) as the map key and deleting the entry on successful completion means getLatestActivity only reflects in-flight or errored sessions. Test coverage at agent-harness-service.test.ts:297–451 verifies the key concurrency and activity-isolation scenarios correctly.

The parseSidepanelAgentChatBody backward-compat fallback (agentSessionId ?? conversationId) is solid — older clients without agentSessionId route by conversation automatically.


Issues Found

1. attachUrl in 409 sidepanel response uses legacy session-less path (agents.ts:207–217)

When agentSessionId is a UUID and startTurn throws TurnAlreadyActiveError, the response sends:

attachUrl: `/agents/${agent.id}/chat/stream?turnId=${err.turnId}`,

But /agents/:id/chat/stream is the main-only stream endpoint. For a UUID session, the correct path is /agents/:id/sessions/:sessionId/chat/stream?turnId=…. A client that uses this URL directly to attach will hit the wrong session stream.

Fix this →

2. Dead sessionId? in runDetachedTurn input type (agent-harness-service.ts:739–748)

The param type accepts sessionId?: AgentSessionId but the body immediately shadows it:

const turn = this.turnRegistry.get(turnId)
const sessionId = turn.sessionId  // ← from registry, not input

The sessionId? field in the input type is dead code — anything passed there is silently ignored, which will mislead future callers. Remove it from the input type.

Fix this →

3. Redundant runtimeStatePath read in loadLatestAgentSessionRecord (acpx-runtime.ts:238–265)

The method reads paths.runtimeStatePath at line 245. If the store lookup fails, it falls through to loadSessionRecord(agent, MAIN_AGENT_SESSION_ID) (line 258). That function also reads paths.runtimeStatePath at line 228. The same file is read twice in the failure path. Pass the already-loaded latestForAgent value into the fallback or inline the final store lookup to avoid the duplicate I/O.

Fix this →

4. setTimeout(r, 5) in concurrent-sessions test (agents.test.ts:705)

A fixed 5 ms delay is used to ensure the first request is in-flight before the second. This is fragile in slow CI. A deterministic approach — waiting until the blocking service has registered the turn — would make it reliable. Greptile's suggestion (await blocking._firstTurnStarted) is a clean fix.

Fix this →


Minor Observations

  • listSessions still hardcodes [{ id: 'main' }] (acpx-runtime.ts:99–109): Previously flagged. Fine as long as no caller enumerates sessions from this method; worth a comment to that effect.

  • enqueueHarnessMessage sends sessionId redundantly (useAgents.ts:363–378): When the session-specific URL path is used (/sessions/:sessionId/queue), the body also includes sessionId. The route handler ignores the body's sessionId field in favor of the path param, so there's no semantic bug — just noise.

  • Agent-level state file reflects sidepanel sessions (acpx-agent-common.ts:81–84): saveLatestRuntimeState writes both runtimeSessionStatePath and runtimeStatePath with the same sessionId. This means after any sidepanel turn, getLatestRowSnapshot will return the sidepanel session's record (including its lastUserMessage and cwd). The test coverage at agent-harness-service.test.ts:350–397 confirms this is intended — the home row always reflects the most recently active session. This is a design choice, not a bug, but worth validating against product intent.


Summary

Four actionable items before merge: the attachUrl mismatch on sidepanel 409s (#1) is the highest priority — clients using that URL to reattach on a UUID session will land on the wrong stream. Items #2 and #3 are the open cleanup items from greptile's previous review. Item #4 is a test reliability concern. The core session-routing logic, concurrency model, and isolation semantics are all sound.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

✅ Tests passed — 1126/1131

Suite Passed Failed Skipped
agent 103/103 0 0
build 14/14 0 0
eval 95/95 0 0
server-agent 246/246 0 0
server-api 94/94 0 0
server-browser 4/4 0 0
server-integration 9/10 0 1
server-lib 273/274 0 1
server-root 59/62 0 3
server-tools 229/229 0 0

View workflow run

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