Skip to content

Commit 99eb967

Browse files
committed
fix(sessions): clear stale top-level spawn lineage
1 parent f870bea commit 99eb967

3 files changed

Lines changed: 52 additions & 8 deletions

File tree

src/agents/agent-command.live-model-switch.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ vi.mock("../routing/session-key.js", async () => {
264264
);
265265
return {
266266
...actual,
267+
isAcpSessionKey: () => false,
267268
isSubagentSessionKey: () => false,
268269
normalizeAgentId: (id: string) => id,
269270
normalizeMainKey: (key?: string | null) => key?.trim() || "main",
@@ -1197,6 +1198,33 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
11971198
expect(lifecycleFinishingCalls).toHaveLength(1);
11981199
});
11991200

1201+
it("does not pass stale sessionEntry.spawnedBy for top-level sessions", async () => {
1202+
const runAttemptCalls: Array<{ spawnedBy?: string }> = [];
1203+
state.runWithModelFallbackMock.mockImplementation(async (params: FallbackRunnerParams) => {
1204+
const result = await params.run(params.provider, params.model);
1205+
return {
1206+
result,
1207+
provider: params.provider,
1208+
model: params.model,
1209+
attempts: [],
1210+
};
1211+
});
1212+
state.runAgentAttemptMock.mockImplementation(async (...args: unknown[]) => {
1213+
const attemptParams = args[0] as { spawnedBy?: string } | undefined;
1214+
runAttemptCalls.push({ spawnedBy: attemptParams?.spawnedBy });
1215+
return makeSuccessResult("anthropic", "claude");
1216+
});
1217+
state.sessionEntryMock = {
1218+
sessionId: "session-1",
1219+
updatedAt: Date.now(),
1220+
spawnedBy: "agent:main:subagent:stale-child",
1221+
};
1222+
1223+
await runBasicAgentCommand();
1224+
1225+
expect(runAttemptCalls[0]?.spawnedBy).toBeUndefined();
1226+
});
1227+
12001228
it("validates explicit thinking against configured model compat without an allowlist", async () => {
12011229
state.runtimeConfigMock = {
12021230
agents: {

src/agents/agent-command.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { createSubsystemLogger } from "../logging/subsystem.js";
2626
import { loadManifestMetadataSnapshot } from "../plugins/manifest-contract-eligibility.js";
2727
import {
2828
classifySessionKeyShape,
29+
isAcpSessionKey,
2930
isUnscopedSessionKeySentinel,
3031
isSubagentSessionKey,
3132
normalizeAgentId,
@@ -1321,7 +1322,11 @@ async function agentCommandInternal(
13211322
});
13221323
for (;;) {
13231324
try {
1324-
const spawnedBy = normalizedSpawned.spawnedBy ?? sessionEntry?.spawnedBy;
1325+
const spawnedBy =
1326+
normalizedSpawned.spawnedBy ??
1327+
(isSubagentSessionKey(sessionKey) || isAcpSessionKey(sessionKey)
1328+
? sessionEntry?.spawnedBy
1329+
: undefined);
13251330
const effectiveFallbacksOverride = resolveEffectiveModelFallbacks({
13261331
cfg,
13271332
agentId: sessionAgentId,

src/auto-reply/reply/session.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ import { createSubsystemLogger } from "../../logging/subsystem.js";
4343
import { closeTrackedBrowserTabsForSessions } from "../../plugin-sdk/browser-maintenance.js";
4444
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
4545
import type { PluginHookSessionEndReason } from "../../plugins/hook-types.js";
46-
import { isAcpSessionKey, normalizeMainKey } from "../../routing/session-key.js";
46+
import {
47+
isAcpSessionKey,
48+
isSubagentSessionKey,
49+
normalizeMainKey,
50+
} from "../../routing/session-key.js";
4751
import { isInterSessionInputProvenance } from "../../sessions/input-provenance.js";
4852
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
4953
import {
@@ -628,6 +632,7 @@ export async function initSessionState(params: {
628632
const lastTo = deliveryFields.lastTo ?? lastToRaw;
629633
const lastAccountId = deliveryFields.lastAccountId ?? lastAccountIdRaw;
630634
const lastThreadId = deliveryFields.lastThreadId ?? lastThreadIdRaw;
635+
const preserveSpawnLineage = isSubagentSessionKey(sessionKey) || isAcpSessionKey(sessionKey);
631636
sessionEntry = {
632637
...baseEntry,
633638
sessionId,
@@ -659,14 +664,20 @@ export async function initSessionState(params: {
659664
cliSessionBindings: baseEntry?.cliSessionBindings,
660665
claudeCliSessionId: baseEntry?.claudeCliSessionId,
661666
label: persistedLabel ?? baseEntry?.label,
662-
spawnedBy: persistedSpawnedBy ?? baseEntry?.spawnedBy,
663-
spawnedWorkspaceDir: persistedSpawnedWorkspaceDir ?? baseEntry?.spawnedWorkspaceDir,
664-
spawnedCwd: persistedSpawnedCwd ?? baseEntry?.spawnedCwd,
667+
spawnedBy: preserveSpawnLineage ? (persistedSpawnedBy ?? baseEntry?.spawnedBy) : undefined,
668+
spawnedWorkspaceDir: preserveSpawnLineage
669+
? (persistedSpawnedWorkspaceDir ?? baseEntry?.spawnedWorkspaceDir)
670+
: undefined,
671+
spawnedCwd: preserveSpawnLineage ? (persistedSpawnedCwd ?? baseEntry?.spawnedCwd) : undefined,
665672
parentSessionKey: persistedParentSessionKey ?? baseEntry?.parentSessionKey,
666673
forkedFromParent: persistedForkedFromParent ?? baseEntry?.forkedFromParent,
667-
spawnDepth: persistedSpawnDepth ?? baseEntry?.spawnDepth,
668-
subagentRole: persistedSubagentRole ?? baseEntry?.subagentRole,
669-
subagentControlScope: persistedSubagentControlScope ?? baseEntry?.subagentControlScope,
674+
spawnDepth: preserveSpawnLineage ? (persistedSpawnDepth ?? baseEntry?.spawnDepth) : undefined,
675+
subagentRole: preserveSpawnLineage
676+
? (persistedSubagentRole ?? baseEntry?.subagentRole)
677+
: undefined,
678+
subagentControlScope: preserveSpawnLineage
679+
? (persistedSubagentControlScope ?? baseEntry?.subagentControlScope)
680+
: undefined,
670681
sendPolicy: baseEntry?.sendPolicy,
671682
queueMode: baseEntry?.queueMode,
672683
queueDebounceMs: baseEntry?.queueDebounceMs,

0 commit comments

Comments
 (0)