Merge pull request #4067 from PeterPonyu/fix/3898-fallback-preserve-team-context
fix(team-mode): preserve team membership across model fallback (#3898)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test"
|
||||
import { tryFallbackRetry, type FallbackRetryHandlerDeps } from "./fallback-retry-handler"
|
||||
import { tryFallbackRetry, TeamModeFallbackError, type FallbackRetryHandlerDeps } from "./fallback-retry-handler"
|
||||
import type { FallbackEntry } from "../../shared/model-requirements"
|
||||
import type { ProviderModelsCache } from "../../shared/connected-providers-cache"
|
||||
import { QUESTION_DENIED_SESSION_PERMISSION } from "../../shared/question-denied-session-permission"
|
||||
@@ -453,4 +453,41 @@ describe("tryFallbackRetry", () => {
|
||||
expect(args.task.model?.modelID).toBe("fallback-model-1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#team-mode fallback", () => {
|
||||
test("throws TeamModeFallbackError when teamRunId is set but onSessionCreated is absent", async () => {
|
||||
// given: a team-mode task that somehow lost its onSessionCreated callback —
|
||||
// without it the fallback session would not be registered in the team-session
|
||||
// registry and every team tool call would silently fail with "not in team"
|
||||
const args = createDefaultArgs({
|
||||
teamRunId: "team-run-abc",
|
||||
onSessionCreated: undefined,
|
||||
})
|
||||
|
||||
// when / then: a bounded structured error must surface instead
|
||||
await expect(tryFallbackRetry(args)).rejects.toThrow(TeamModeFallbackError)
|
||||
await expect(tryFallbackRetry(createDefaultArgs({ teamRunId: "team-run-abc", onSessionCreated: undefined }))).rejects.toThrow(
|
||||
"team-mode fallback denied: cannot preserve team context",
|
||||
)
|
||||
})
|
||||
|
||||
test("proceeds normally when teamRunId and onSessionCreated are both present", async () => {
|
||||
// given: a properly-formed team-mode task with its session registration callback
|
||||
const onSessionCreated = mock(async () => {})
|
||||
const args = createDefaultArgs({
|
||||
teamRunId: "team-run-abc",
|
||||
onSessionCreated,
|
||||
})
|
||||
|
||||
// when
|
||||
const result = await tryFallbackRetry(args)
|
||||
|
||||
// then: fallback is queued and the retry input preserves both team fields
|
||||
expect(result).toBe(true)
|
||||
const key = `${args.task.model!.providerID}/${args.task.model!.modelID}`
|
||||
const retryInput = args.queuesByKey.get(key)?.[0]?.input
|
||||
expect(retryInput?.teamRunId).toBe("team-run-abc")
|
||||
expect(retryInput?.onSessionCreated).toBe(onSessionCreated)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,6 +13,13 @@ import { transformModelForProvider } from "../../shared/provider-model-id-transf
|
||||
import { abortWithTimeout } from "./abort-with-timeout"
|
||||
import { ensureCurrentAttempt, scheduleRetryAttempt } from "./attempt-lifecycle"
|
||||
|
||||
export class TeamModeFallbackError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = "TeamModeFallbackError"
|
||||
}
|
||||
}
|
||||
|
||||
function canonicalizeModelID(modelID: string): string {
|
||||
return modelID.toLowerCase().replace(/\./g, "-")
|
||||
}
|
||||
@@ -183,6 +190,21 @@ export async function tryFallbackRetry(args: {
|
||||
nextModel: `${providerID}/${transformedModelId}`,
|
||||
})
|
||||
|
||||
// Guard: a team-mode task (teamRunId set) MUST carry an onSessionCreated callback so
|
||||
// the fallback session gets registered in the team-session registry under the original
|
||||
// member slot. Without it the new session would not appear as a team participant and
|
||||
// every subsequent team tool call would throw "not in team". Fail with a bounded
|
||||
// structured error instead of silently entering that confusing runtime state.
|
||||
if (task.teamRunId && !task.onSessionCreated) {
|
||||
deps.log("[background-agent] team-mode fallback denied: task has teamRunId but no onSessionCreated; cannot preserve team membership", {
|
||||
taskId: task.id,
|
||||
teamRunId: task.teamRunId,
|
||||
})
|
||||
throw new TeamModeFallbackError(
|
||||
`team-mode fallback denied: cannot preserve team context for task ${task.id} (teamRunId=${task.teamRunId})`,
|
||||
)
|
||||
}
|
||||
|
||||
const key = task.model ? `${task.model.providerID}/${task.model.modelID}` : task.agent
|
||||
const queue = queuesByKey.get(key) ?? []
|
||||
const retryInput: LaunchInput = {
|
||||
|
||||
Reference in New Issue
Block a user