diff --git a/src/auto-reply/reply/session.test.ts b/src/auto-reply/reply/session.test.ts index afa1c4be1ca..da635497125 100644 --- a/src/auto-reply/reply/session.test.ts +++ b/src/auto-reply/reply/session.test.ts @@ -1405,6 +1405,60 @@ describe("initSessionState preserves behavior overrides across /new and /reset", } }); + it("preserves spawned session ownership metadata across /new and /reset", async () => { + const storePath = await createStorePath("openclaw-reset-spawned-metadata-"); + const sessionKey = "subagent:owned-child"; + const existingSessionId = "existing-session-owned-child"; + const overrides = { + spawnedBy: "agent:main:main", + spawnedWorkspaceDir: "/tmp/child-workspace", + parentSessionKey: "agent:main:main", + forkedFromParent: true, + spawnDepth: 2, + subagentRole: "orchestrator", + subagentControlScope: "children", + displayName: "Ops Child", + } as const; + const cases = [ + { name: "new preserves spawned session ownership metadata", body: "/new" }, + { name: "reset preserves spawned session ownership metadata", body: "/reset" }, + ] as const; + + for (const testCase of cases) { + await seedSessionStoreWithOverrides({ + storePath, + sessionKey, + sessionId: existingSessionId, + overrides: { ...overrides }, + }); + + const cfg = { + session: { store: storePath, idleMinutes: 999 }, + } as OpenClawConfig; + + const result = await initSessionState({ + ctx: { + Body: testCase.body, + RawBody: testCase.body, + CommandBody: testCase.body, + From: "user-owned-child", + To: "bot", + ChatType: "direct", + SessionKey: sessionKey, + Provider: "telegram", + Surface: "telegram", + }, + cfg, + commandAuthorized: true, + }); + + expect(result.isNewSession, testCase.name).toBe(true); + expect(result.resetTriggered, testCase.name).toBe(true); + expect(result.sessionId, testCase.name).not.toBe(existingSessionId); + expect(result.sessionEntry).toMatchObject(overrides); + } + }); + it("requires operator.admin when Provider is internal even if Surface carries external metadata", async () => { const storePath = await createStorePath("openclaw-internal-reset-provider-authoritative-"); const sessionKey = "agent:main:telegram:dm:provider-authoritative"; diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 37c0b3e772f..3b4f238939d 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -263,6 +263,14 @@ export async function initSessionState(params: { let persistedCliSessionBindings: SessionEntry["cliSessionBindings"]; let persistedClaudeCliSessionId: string | undefined; let persistedLabel: string | undefined; + let persistedSpawnedBy: SessionEntry["spawnedBy"]; + let persistedSpawnedWorkspaceDir: SessionEntry["spawnedWorkspaceDir"]; + let persistedParentSessionKey: SessionEntry["parentSessionKey"]; + let persistedForkedFromParent: SessionEntry["forkedFromParent"]; + let persistedSpawnDepth: SessionEntry["spawnDepth"]; + let persistedSubagentRole: SessionEntry["subagentRole"]; + let persistedSubagentControlScope: SessionEntry["subagentControlScope"]; + let persistedDisplayName: SessionEntry["displayName"]; const normalizedChatType = normalizeChatType(ctx.ChatType); const isGroup = @@ -424,6 +432,14 @@ export async function initSessionState(params: { persistedCliSessionBindings = entry.cliSessionBindings; persistedClaudeCliSessionId = entry.claudeCliSessionId; persistedLabel = entry.label; + persistedSpawnedBy = entry.spawnedBy; + persistedSpawnedWorkspaceDir = entry.spawnedWorkspaceDir; + persistedParentSessionKey = entry.parentSessionKey; + persistedForkedFromParent = entry.forkedFromParent; + persistedSpawnDepth = entry.spawnDepth; + persistedSubagentRole = entry.subagentRole; + persistedSubagentControlScope = entry.subagentControlScope; + persistedDisplayName = entry.displayName; } } @@ -483,12 +499,19 @@ export async function initSessionState(params: { cliSessionBindings: persistedCliSessionBindings ?? baseEntry?.cliSessionBindings, claudeCliSessionId: persistedClaudeCliSessionId ?? baseEntry?.claudeCliSessionId, label: persistedLabel ?? baseEntry?.label, + spawnedBy: persistedSpawnedBy ?? baseEntry?.spawnedBy, + spawnedWorkspaceDir: persistedSpawnedWorkspaceDir ?? baseEntry?.spawnedWorkspaceDir, + parentSessionKey: persistedParentSessionKey ?? baseEntry?.parentSessionKey, + forkedFromParent: persistedForkedFromParent ?? baseEntry?.forkedFromParent, + spawnDepth: persistedSpawnDepth ?? baseEntry?.spawnDepth, + subagentRole: persistedSubagentRole ?? baseEntry?.subagentRole, + subagentControlScope: persistedSubagentControlScope ?? baseEntry?.subagentControlScope, sendPolicy: baseEntry?.sendPolicy, queueMode: baseEntry?.queueMode, queueDebounceMs: baseEntry?.queueDebounceMs, queueCap: baseEntry?.queueCap, queueDrop: baseEntry?.queueDrop, - displayName: baseEntry?.displayName, + displayName: persistedDisplayName ?? baseEntry?.displayName, chatType: baseEntry?.chatType, channel: baseEntry?.channel, groupId: baseEntry?.groupId,