-
Notifications
You must be signed in to change notification settings - Fork 2
Pin CTO and worker identity sessions to the primary lane #179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dbf9c48
05bd5c1
20bc2a5
495b64c
cba2720
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,11 @@ import { | |
| shouldFlushBufferedAssistantTextForEvent, | ||
| type BufferedAssistantText, | ||
| } from "./chatTextBatching"; | ||
| import { | ||
| isPrimaryPinnedIdentity, | ||
| normalizeIdentityPermissionMode, | ||
| resolveIdentityExecutionLane, | ||
| } from "./identitySessionPolicy"; | ||
| import type { Logger } from "../logging/logger"; | ||
| import type { createLaneService } from "../lanes/laneService"; | ||
| import { resolveLaneLaunchContext, type LaneLaunchContext } from "../lanes/laneLaunchContext"; | ||
|
|
@@ -2416,17 +2421,6 @@ function inferCapabilityMode(provider: AgentChatProvider): CtoCapabilityMode { | |
| return provider === "codex" || provider === "claude" || provider === "cursor" || provider === "opencode" ? "full_tooling" : "fallback"; | ||
| } | ||
|
|
||
| function guardedIdentityPermissionModeForProvider(_provider: AgentChatProvider): AgentChatSession["permissionMode"] { | ||
| return "plan"; | ||
| } | ||
|
|
||
| function normalizeIdentityPermissionMode( | ||
| mode: AgentChatSession["permissionMode"] | undefined, | ||
| provider: AgentChatProvider, | ||
| ): AgentChatSession["permissionMode"] { | ||
| return mode === "plan" ? "plan" : guardedIdentityPermissionModeForProvider(provider); | ||
| } | ||
|
|
||
| function isLightweightSession(session: Pick<AgentChatSession, "sessionProfile">): boolean { | ||
| return session.sessionProfile === "light"; | ||
| } | ||
|
|
@@ -4791,9 +4785,12 @@ export function createAgentChatService(args: { | |
| const resolvePrimaryIdentityLane = async (): Promise<string> => { | ||
| await laneService.ensurePrimaryLane?.().catch(() => {}); | ||
| const lanes = await laneService.list({ includeArchived: false, includeStatus: false }); | ||
| const primary = lanes.find((lane) => lane.laneType === "primary") ?? lanes[0] ?? null; | ||
| // Identity sessions (CTO + worker agents) must pin to the actual primary | ||
| // lane. Never fall back to lanes[0] — that would silently land the | ||
| // identity on a foreign lane, defeating the whole contract. | ||
| const primary = lanes.find((lane) => lane.laneType === "primary") ?? null; | ||
| if (!primary?.id) { | ||
| throw new Error("No lane is available to host the canonical identity chat session."); | ||
| throw new Error("No primary lane is available to host the canonical identity chat session."); | ||
| } | ||
| return primary.id; | ||
| }; | ||
|
|
@@ -10379,11 +10376,22 @@ export function createAgentChatService(args: { | |
| : undefined; | ||
| const normalizedCursorConfigValues = normalizeCursorConfigValueRecord(requestedCursorConfigValues); | ||
| const capabilityMode = inferCapabilityMode(effectiveProvider); | ||
| // Identity-pinned sessions (CTO + worker agents) are locked to full-auto. | ||
| // Discard the native provider permission overrides supplied by callers so | ||
| // they cannot smuggle `plan` / `ask` / read-only sandboxes past the lock | ||
| // via claudePermissionMode / codexApprovalPolicy / codexSandbox / | ||
| // opencodePermissionMode. | ||
| const identityPinned = isPrimaryPinnedIdentity(identityKey); | ||
| const effectiveInteractionMode = identityPinned ? undefined : requestedInteractionMode; | ||
| const effectiveClaudePermissionMode = identityPinned ? undefined : requestedClaudePermissionMode; | ||
| const effectiveCodexApprovalPolicy = identityPinned ? undefined : requestedCodexApprovalPolicy; | ||
| const effectiveCodexSandbox = identityPinned ? undefined : requestedCodexSandbox; | ||
| const effectiveCodexConfigSource = identityPinned ? undefined : requestedCodexConfigSource; | ||
| let effectivePermissionMode = identityKey | ||
| ? normalizeIdentityPermissionMode(requestedPermMode, effectiveProvider) | ||
| ? normalizeIdentityPermissionMode(identityKey, requestedPermMode, effectiveProvider) | ||
| : requestedPermMode; | ||
| const chatConfig = resolveChatConfig(); | ||
| let requestedOpenCodePermissionMode = requestedOpenCodePermissionModeArg; | ||
| let requestedOpenCodePermissionMode = identityPinned ? undefined : requestedOpenCodePermissionModeArg; | ||
| const localHarnessPermissions = applyLocalHarnessPermissionMode({ | ||
| descriptor: resolvedDescriptor, | ||
| requestedPermissionMode: effectivePermissionMode, | ||
|
|
@@ -10394,14 +10402,14 @@ export function createAgentChatService(args: { | |
|
|
||
| const nativePermissionFields = (() => { | ||
| if (effectiveProvider === "claude") { | ||
| const interactionMode = requestedInteractionMode | ||
| ?? (requestedClaudePermissionMode === "plan" ? "plan" : undefined) | ||
| const interactionMode = effectiveInteractionMode | ||
| ?? (effectiveClaudePermissionMode === "plan" ? "plan" : undefined) | ||
| ?? (effectivePermissionMode === "plan" ? "plan" : undefined) | ||
| ?? (chatConfig.claudePermissionMode === "plan" ? "plan" : undefined) | ||
| ?? "default"; | ||
| const claudePermissionMode = requestedClaudePermissionMode | ||
| const claudePermissionMode = effectiveClaudePermissionMode | ||
| ? resolveSessionClaudeAccessMode( | ||
| { claudePermissionMode: requestedClaudePermissionMode, permissionMode: undefined }, | ||
| { claudePermissionMode: effectiveClaudePermissionMode, permissionMode: undefined }, | ||
| chatConfig.claudePermissionMode, | ||
| ) | ||
| : resolveSessionClaudeAccessMode( | ||
|
|
@@ -10411,17 +10419,17 @@ export function createAgentChatService(args: { | |
| return { interactionMode, claudePermissionMode }; | ||
| } | ||
| if (effectiveProvider === "codex") { | ||
| const codexConfigSource = requestedCodexConfigSource | ||
| const codexConfigSource = effectiveCodexConfigSource | ||
| ?? legacyPermissionModeToCodexConfigSource(effectivePermissionMode) | ||
| ?? "flags"; | ||
| if (codexConfigSource === "config-toml") { | ||
| return { codexConfigSource }; | ||
| } | ||
| return { | ||
| codexApprovalPolicy: requestedCodexApprovalPolicy | ||
| codexApprovalPolicy: effectiveCodexApprovalPolicy | ||
| ?? legacyPermissionModeToCodexApprovalPolicy(effectivePermissionMode) | ||
| ?? chatConfig.codexApprovalPolicy, | ||
| codexSandbox: requestedCodexSandbox | ||
| codexSandbox: effectiveCodexSandbox | ||
| ?? legacyPermissionModeToCodexSandbox(effectivePermissionMode) | ||
| ?? chatConfig.codexSandboxMode, | ||
| codexConfigSource, | ||
|
|
@@ -12300,7 +12308,21 @@ export function createAgentChatService(args: { | |
| }; | ||
|
|
||
| const resumeSession = async ({ sessionId }: { sessionId: string }): Promise<AgentChatSession> => { | ||
| const managed = ensureManagedSession(sessionId); | ||
| let managed = ensureManagedSession(sessionId); | ||
|
|
||
| // Identity-pinned sessions (CTO + worker agents) must always run on the | ||
| // canonical primary lane. If a persisted row points at a foreign lane | ||
| // (e.g. pre-pinning session, or the previous primary was archived), | ||
| // migrate it to the canonical lane before we spin up a runtime. | ||
| if (isPrimaryPinnedIdentity(managed.session.identityKey) && managed.session.laneId) { | ||
| const canonicalLaneId = await resolvePrimaryIdentityLane(); | ||
| if (canonicalLaneId && managed.session.laneId !== canonicalLaneId) { | ||
| sessionService.updateMeta({ sessionId, laneId: canonicalLaneId }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [🟠 High] [🔵 Bug] This migration only rewrites the session row’s host |
||
| managedSessions.delete(sessionId); | ||
| managed = ensureManagedSession(sessionId); | ||
| } | ||
| } | ||
|
|
||
| refreshManagedLaneLaunchContext(managed, { purpose: "resume this chat" }); | ||
| const persisted = readPersistedState(sessionId); | ||
| managed.session.capabilityMode = managed.session.capabilityMode ?? inferCapabilityMode(managed.session.provider); | ||
|
|
@@ -12554,7 +12576,11 @@ export function createAgentChatService(args: { | |
| } | ||
|
|
||
| const canonicalLaneId = await resolvePrimaryIdentityLane(); | ||
| const selectedExecutionLaneId = requestedLaneId || null; | ||
| const selectedExecutionLaneId = resolveIdentityExecutionLane( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [🟡 Medium] [🔵 Bug] This change now persists the canonical lane as the session’s default execution lane, not just the hosted lane: // apps/desktop/src/main/services/chat/agentChatService.ts
const selectedExecutionLaneId = resolveIdentityExecutionLane(
args.identityKey,
requestedLaneId,
canonicalLaneId,
);
|
||
| args.identityKey, | ||
| requestedLaneId, | ||
| canonicalLaneId, | ||
| ); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| const existing = await listSessions(undefined, { includeIdentity: true }); | ||
| const identitySessions = existing | ||
| .filter((entry) => entry.identityKey === args.identityKey) | ||
|
|
@@ -12566,13 +12592,19 @@ export function createAgentChatService(args: { | |
|
|
||
| const preferred = canonicalExisting; | ||
| if (preferred) { | ||
| // `canonicalExisting` is already filtered to `entry.laneId === canonicalLaneId`, | ||
| // so `preferred` is guaranteed to be on the canonical lane here — no | ||
| // migration guard needed. Foreign-lane legacy sessions are left untouched | ||
| // (see the `does not reuse a foreign-lane identity session` test); a | ||
| // fresh canonical session will be created below when none is found. | ||
| const managed = ensureManagedSession(preferred.sessionId); | ||
| managed.session.identityKey = args.identityKey; | ||
| managed.session.capabilityMode = inferCapabilityMode(managed.session.provider); | ||
| if (args.reasoningEffort) { | ||
| managed.session.reasoningEffort = normalizeReasoningEffort(args.reasoningEffort); | ||
| } | ||
| managed.session.permissionMode = normalizeIdentityPermissionMode( | ||
| args.identityKey, | ||
| args.permissionMode ?? managed.session.permissionMode, | ||
| managed.session.provider, | ||
| ); | ||
|
|
@@ -12640,7 +12672,7 @@ export function createAgentChatService(args: { | |
| model: preferredModel, | ||
| ...(resolvedModelId ? { modelId: resolvedModelId } : {}), | ||
| reasoningEffort: args.reasoningEffort ?? pref?.reasoningEffort ?? null, | ||
| permissionMode: args.permissionMode ?? "plan", | ||
| ...(args.permissionMode ? { permissionMode: args.permissionMode } : {}), | ||
| identityKey: args.identityKey | ||
| }); | ||
|
|
||
|
|
@@ -13272,6 +13304,7 @@ export function createAgentChatService(args: { | |
| const managed = ensureManagedSession(sessionId); | ||
| const chatConfig = resolveChatConfig(); | ||
| const isIdentitySession = Boolean(managed.session.identityKey); | ||
| const identityPinned = isPrimaryPinnedIdentity(managed.session.identityKey); | ||
| const hasConversation = managed.recentConversationEntries.length > 0 || readTranscriptConversationEntries(managed).length > 0; | ||
| const prevCodexApprovalPolicy = managed.session.codexApprovalPolicy; | ||
| const prevCodexSandbox = managed.session.codexSandbox; | ||
|
|
@@ -13338,6 +13371,7 @@ export function createAgentChatService(args: { | |
|
|
||
| if (isIdentitySession) { | ||
| managed.session.permissionMode = normalizeIdentityPermissionMode( | ||
| managed.session.identityKey, | ||
| managed.session.permissionMode, | ||
| nextProvider, | ||
| ); | ||
|
|
@@ -13393,36 +13427,40 @@ export function createAgentChatService(args: { | |
|
|
||
| if (permissionMode !== undefined) { | ||
| managed.session.permissionMode = isIdentitySession | ||
| ? normalizeIdentityPermissionMode(permissionMode, managed.session.provider) | ||
| ? normalizeIdentityPermissionMode(managed.session.identityKey, permissionMode, managed.session.provider) | ||
| : permissionMode; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| applyLegacyPermissionModeToNativeControls(managed.session, managed.session.permissionMode); | ||
| } | ||
|
|
||
| if (interactionMode !== undefined) { | ||
| // Identity-pinned sessions (CTO + worker agents) are locked to full-auto. | ||
| // Ignore incoming native permission overrides — applyLegacyPermissionMode- | ||
| // ToNativeControls() has already derived the correct native fields from | ||
| // full-auto, and we must not let callers layer a stricter mode on top. | ||
| if (interactionMode !== undefined && !identityPinned) { | ||
| managed.session.interactionMode = interactionMode; | ||
| } | ||
|
|
||
| if (claudePermissionMode !== undefined) { | ||
| if (claudePermissionMode !== undefined && !identityPinned) { | ||
| if (claudePermissionMode === "plan") { | ||
| managed.session.interactionMode = "plan"; | ||
| } else { | ||
| managed.session.claudePermissionMode = claudePermissionMode; | ||
| } | ||
| } | ||
|
|
||
| if (codexApprovalPolicy !== undefined) { | ||
| if (codexApprovalPolicy !== undefined && !identityPinned) { | ||
| managed.session.codexApprovalPolicy = codexApprovalPolicy; | ||
| } | ||
|
|
||
| if (codexSandbox !== undefined) { | ||
| if (codexSandbox !== undefined && !identityPinned) { | ||
| managed.session.codexSandbox = codexSandbox; | ||
| } | ||
|
|
||
| if (codexConfigSource !== undefined) { | ||
| if (codexConfigSource !== undefined && !identityPinned) { | ||
| managed.session.codexConfigSource = codexConfigSource; | ||
| } | ||
|
|
||
| if (opencodePermissionMode !== undefined) { | ||
| if (opencodePermissionMode !== undefined && !identityPinned) { | ||
| managed.session.opencodePermissionMode = opencodePermissionMode; | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.