Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions packages/coding-agent/src/core/agent-session-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,10 @@ export class AgentSessionRuntime implements SubagentRuntimeHost {
async createRlmSubagentRuntime(options: CreateRlmSubagentRuntimeOptions): Promise<RlmSubagentRuntime> {
const sessionManager = SessionManager.create(options.parentSession.sessionManager.getCwd(), options.sessionDir);
if (options.parentSession.sessionFile) {
sessionManager.newSession({ parentSession: options.parentSession.sessionFile });
sessionManager.newSession({
parentSession: options.parentSession.sessionFile,
rlmDepth: options.rlmDepth,
});
}
const runtime = await this.scopedBuild(() =>
createAgentSessionRuntime(this.createRuntime, {
Expand Down Expand Up @@ -570,8 +573,12 @@ export class AgentSessionRuntime implements SubagentRuntimeHost {
}
const sessionDir = this.session.sessionManager.getSessionDir();
if (!targetLeafId) {
const sourceHeader = this.session.sessionManager.getHeader();
const sessionManager = SessionManager.create(this.cwd, sessionDir);
sessionManager.newSession({ parentSession: currentSessionFile });
Comment thread
snimu marked this conversation as resolved.
sessionManager.newSession({
parentSession: currentSessionFile,
rlmDepth: sourceHeader?.rlmDepth,
});
Comment thread
snimu marked this conversation as resolved.
const lease = this.acquireReplacementLease(sessionManager.getSessionFile());
Comment thread
snimu marked this conversation as resolved.
await this.teardownForReplacement("fork", sessionManager.getSessionFile(), lease);
await this.buildAndApplyReplacement(
Expand Down Expand Up @@ -618,7 +625,11 @@ export class AgentSessionRuntime implements SubagentRuntimeHost {

const sessionManager = this.session.sessionManager;
if (!targetLeafId) {
sessionManager.newSession({ parentSession: this.session.sessionFile });
const sourceHeader = sessionManager.getHeader();
sessionManager.newSession({
parentSession: this.session.sessionFile,
rlmDepth: sourceHeader?.rlmDepth,
});
} else {
sessionManager.createBranchedSession(targetLeafId);
}
Expand Down
15 changes: 13 additions & 2 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1231,7 +1231,10 @@ export class AgentSession {
this._mcpManager = config.mcpManager;
this._baseToolsOverride = config.baseToolsOverride;
this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
this._rlmDepth = config.rlmDepth ?? parseDepth(process.env.RLM_DEPTH, 0, "RLM_DEPTH");
this._rlmDepth =
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
config.rlmDepth ??
this.sessionManager.getHeader()?.rlmDepth ??
parseDepth(process.env.RLM_DEPTH, 0, "RLM_DEPTH");
Comment thread
snimu marked this conversation as resolved.
this._rlmMaxDepth = config.rlmMaxDepth ?? parseDepth(process.env.RLM_MAX_DEPTH, 1, "RLM_MAX_DEPTH");
this._prewarmIpythonKernel = (config.prewarmIpythonKernel ?? false) && this._rlmDepth === 0;
this._autoRefineReviewer = config.autoRefineReviewer;
Expand Down Expand Up @@ -3990,6 +3993,11 @@ export class AgentSession {
return this.sessionManager.getSessionId();
}

/** Current RLM spawn depth for this session. */
get rlmDepth(): number {
return this._rlmDepth;
}

/** Current session display name, if set */
get sessionName(): string | undefined {
return this.sessionManager.getSessionName();
Expand Down Expand Up @@ -8625,7 +8633,10 @@ export class AgentSession {
private _createInlineRlmSubagentRuntime(options: CreateRlmSubagentRuntimeOptions): RlmSubagentRuntime {
const childSessionManager = SessionManager.create(this._cwd, options.sessionDir);
if (options.parentSession.sessionFile) {
childSessionManager.newSession({ parentSession: options.parentSession.sessionFile });
childSessionManager.newSession({
parentSession: options.parentSession.sessionFile,
rlmDepth: options.rlmDepth,
});
}
childSessionManager.appendModelChange(options.model.provider, options.model.id);
childSessionManager.appendThinkingLevelChange(options.thinkingLevel);
Expand Down
111 changes: 110 additions & 1 deletion packages/coding-agent/src/core/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,16 @@ export interface SessionHeader {
timestamp: string;
cwd: string;
parentSession?: string;
/** RLM spawn depth. Optional for backward compatibility. Forks preserve the source depth. */
rlmDepth?: number;
git?: GitContext;
}

export interface NewSessionOptions {
id?: string;
parentSession?: string;
/** Explicit RLM spawn depth. An explicitly undefined value suppresses parent derivation. */
rlmDepth?: number;
}

export type SessionPersistListener = (sessionFile: string) => void;
Expand Down Expand Up @@ -291,6 +295,8 @@ export interface SessionInfo {
state?: SessionState;
/** Path to the parent session (if this session was forked). */
parentSessionPath?: string;
/** Resolved RLM spawn depth. */
rlmDepth: number;
created: Date;
modified: Date;
messageCount: number;
Expand Down Expand Up @@ -692,6 +698,82 @@ function readSessionHeader(filePath: string): Partial<SessionHeader> | undefined
return JSON.parse(firstLine) as Partial<SessionHeader>;
}

function isValidRlmDepth(value: unknown): value is number {
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
}

export function resolveSessionRlmDepth(
header: { rlmDepth?: number; parentSession?: string },
sessionPath: string,
): number {
return resolveLegacySessionRlmDepth(header, sessionPath, new Set()) ?? legacyChildDepthFromPath(sessionPath);
}

function resolveLegacySessionRlmDepth(
header: { rlmDepth?: number; parentSession?: string },
sessionPath: string,
visitedPaths: Set<string>,
): number | undefined {
if (isValidRlmDepth(header.rlmDepth)) {
return header.rlmDepth;
}
if (!header.parentSession) {
return 0;
}

const resolvedSessionPath = resolve(sessionPath);
if (visitedPaths.has(resolvedSessionPath)) {
return undefined;
}
visitedPaths.add(resolvedSessionPath);

const pathDepth = legacyChildDepthFromPath(sessionPath);
const parentSessionPath = resolve(dirname(sessionPath), header.parentSession);
try {
Comment thread
snimu marked this conversation as resolved.
const parentHeader = readSessionHeader(parentSessionPath);
if (parentHeader) {
const parentDepth = resolveLegacySessionRlmDepth(parentHeader, parentSessionPath, visitedPaths);
if (parentDepth !== undefined) {
return pathDepth > 0 ? parentDepth + 1 : parentDepth;
}
}
Comment thread
snimu marked this conversation as resolved.
} catch {
// Fall back to artifact ancestry for unavailable or invalid legacy parents.
} finally {
visitedPaths.delete(resolvedSessionPath);
}
return pathDepth;
}

function legacyChildDepthFromPath(sessionPath: string): number {
let depth = 0;
for (const segment of dirname(sessionPath)
.split(/[\\/]+/)
.reverse()) {
if (!/^sub-[0-9a-f]{8}$/.test(segment)) {
break;
}
depth += 1;
}
return depth;
}
Comment thread
cursor[bot] marked this conversation as resolved.

function deriveChildRlmDepth(parentHeader: Partial<SessionHeader> | undefined): number | undefined {
return isValidRlmDepth(parentHeader?.rlmDepth) ? parentHeader.rlmDepth + 1 : undefined;
}
Comment thread
snimu marked this conversation as resolved.

function rootRlmDepthFromEnv(): number {
Comment thread
snimu marked this conversation as resolved.
const value = process.env.RLM_DEPTH;
if (value === undefined || value === "") {
return 0;
}
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed) || parsed < 0) {
throw new Error("RLM_DEPTH must be a non-negative integer");
}
return parsed;
Comment thread
snimu marked this conversation as resolved.
}

function isValidSessionFile(filePath: string): boolean {
try {
const header = readSessionHeader(filePath);
Expand Down Expand Up @@ -1021,6 +1103,7 @@ async function scanSessionInfo(filePath: string, stats: Awaited<ReturnType<typeo
if (!header) return null;
const cwd = typeof header.cwd === "string" ? header.cwd : "";
const parentSessionPath = header.parentSession;
const rlmDepth = resolveSessionRlmDepth(header, filePath);
const modified = getSessionModifiedDateFromLastActivity(lastActivityTime, header, stats.mtime);

return {
Expand All @@ -1030,6 +1113,7 @@ async function scanSessionInfo(filePath: string, stats: Awaited<ReturnType<typeo
name,
state,
parentSessionPath,
rlmDepth,
created: new Date(header.timestamp),
modified,
messageCount,
Expand Down Expand Up @@ -1161,7 +1245,12 @@ export class SessionManager {
const header = this.fileEntries.find((e) => e.type === "session") as SessionHeader | undefined;
this.sessionId = header?.id ?? createSessionId();

if (migrateToCurrentVersion(this.fileEntries)) {
let shouldRewrite = migrateToCurrentVersion(this.fileEntries);
if (header?.parentSession && !isValidRlmDepth(header.rlmDepth)) {
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
header.rlmDepth = resolveSessionRlmDepth(header, this.sessionFile);
shouldRewrite = true;
}
if (shouldRewrite) {
this._rewriteFile();
}

Expand All @@ -1177,6 +1266,15 @@ export class SessionManager {
newSession(options?: NewSessionOptions): string | undefined {
let sessionId = options?.id ?? createSessionId();
let sessionFile: string | undefined;
const hasExplicitRlmDepth = options !== undefined && Object.hasOwn(options, "rlmDepth");
let parentHeader: Partial<SessionHeader> | undefined;
if (options?.parentSession && !hasExplicitRlmDepth) {
try {
parentHeader = readSessionHeader(options.parentSession);
} catch {
// Legacy-invalid or unavailable parents leave the child depth unknown.
}
}
if (this.persist) {
if (options?.id) {
sessionFile = getSessionFilePath(this.getSessionDir(), sessionId);
Expand All @@ -1193,13 +1291,19 @@ export class SessionManager {
this.sessionId = sessionId;
const timestamp = new Date().toISOString();
const git = this.persist ? (captureGitContext(this.cwd) ?? undefined) : undefined;
const rlmDepth = hasExplicitRlmDepth
? options?.rlmDepth
: options?.parentSession
? deriveChildRlmDepth(parentHeader)
: rootRlmDepthFromEnv();
const header: SessionHeader = {
type: "session",
version: CURRENT_SESSION_VERSION,
id: this.sessionId,
timestamp,
cwd: this.cwd,
parentSession: options?.parentSession,
rlmDepth,
git,
};
this.fileEntries = [header];
Expand Down Expand Up @@ -1305,6 +1409,7 @@ export class SessionManager {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
const previousHeader = this.getHeader();
const target = createUniqueSessionFileTarget(dir);
this.sessionDir = dir;
this.sessionId = target.sessionId;
Expand All @@ -1318,6 +1423,8 @@ export class SessionManager {
id: this.sessionId,
timestamp,
cwd: this.cwd,
parentSession: previousHeader?.parentSession,
rlmDepth: resolveSessionRlmDepth(previousHeader ?? {}, target.sessionFile),
git,
};
this.fileEntries = [header, ...this.getEntries()];
Expand Down Expand Up @@ -1941,6 +2048,7 @@ export class SessionManager {
timestamp,
cwd: this.cwd,
parentSession: this.persist ? previousSessionFile : undefined,
rlmDepth: resolveSessionRlmDepth(this.getHeader() ?? {}, previousSessionFile ?? newSessionFile ?? ""),
git: this.persist ? (captureGitContext(this.cwd) ?? undefined) : undefined,
};

Expand Down Expand Up @@ -2134,6 +2242,7 @@ export class SessionManager {
timestamp,
cwd: targetCwd,
parentSession: sourcePath,
rlmDepth: resolveSessionRlmDepth(sourceHeader, sourcePath),
git: captureGitContext(targetCwd) ?? undefined,
};
appendFileSync(newSessionFile, `${JSON.stringify(newHeader)}\n`);
Expand Down
2 changes: 2 additions & 0 deletions packages/coding-agent/src/modes/agent-connection/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface AgentConnectionSessionHeader {
timestamp: string;
cwd: string;
parentSession?: string;
rlmDepth?: number;
git?: {
repoUrl?: string;
commit?: string;
Expand Down Expand Up @@ -122,6 +123,7 @@ export interface AgentConnectionSavedSessionInfo {
name?: string;
state?: AgentConnectionSavedSessionState;
parentSessionPath?: string;
rlmDepth?: number;
created: Date;
modified: Date;
messageCount: number;
Expand Down
37 changes: 15 additions & 22 deletions packages/coding-agent/src/modes/daemon/daemon-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,12 @@ import type {
} from "../../core/rlm-runtime.js";
import { deleteSessionFile } from "../../core/session-file-actions.js";
import { acquireSessionLease, type SessionLease } from "../../core/session-lease.js";
import { readSessionInfo, type SessionInfo, SessionManager } from "../../core/session-manager.js";
import {
readSessionInfo,
resolveSessionRlmDepth,
type SessionInfo,
SessionManager,
} from "../../core/session-manager.js";
import { resolveSessionPath } from "../../core/session-resolver.js";
import type { SessionStats } from "../../core/session-stats.js";
import { type SideQuestionRun, startSideQuestion } from "../../core/side-question.js";
Expand Down Expand Up @@ -141,7 +146,6 @@ import {
type DaemonCommand,
type DaemonOutbound,
type DaemonResponse,
type DaemonSavedSessionInfo,
type DaemonSessionClosedReason,
type DaemonSessionSnapshot,
type DaemonUpdateRestartManifest,
Expand Down Expand Up @@ -186,6 +190,7 @@ import {
SESSION_LEASES_ENABLED_ENV,
} from "./daemon-worker-protocol.js";
import { MutationDrainLatch } from "./mutation-drain-latch.js";
import { serializeSavedSessionInfo } from "./saved-session-info.js";
import {
createSnapshotTranscriptChunks,
SNAPSHOT_TARGET_CHUNK_BYTES,
Expand Down Expand Up @@ -1070,9 +1075,9 @@ export class AgentDaemon {
parentEntry?.sessionFile ??
passive.rootParentState?.runtime.session.sessionFile ??
passive.rootInfo?.path,
rlmDepth: passive.entry.rlmDepth ?? passive.info.rlmDepth,
rlmChildId: passive.entry.childId,
rlmParentNodeId: passive.entry.rlmParentNodeId ?? passive.entry.childId,
...(!passive.rootParentState ? { rlmDepth: passive.entry.rlmDepth } : {}),
spawnCode: passive.entry.spawnCode,
};
});
Expand Down Expand Up @@ -2146,7 +2151,10 @@ export class AgentDaemon {
): Promise<AgentSessionRuntime> {
const sessionManager = SessionManager.create(options.parentSession.sessionManager.getCwd(), options.sessionDir);
if (options.parentSession.sessionFile) {
sessionManager.newSession({ parentSession: options.parentSession.sessionFile });
sessionManager.newSession({
parentSession: options.parentSession.sessionFile,
rlmDepth: options.rlmDepth,
});
Comment thread
snimu marked this conversation as resolved.
}
Comment thread
snimu marked this conversation as resolved.
let stateRef: ActiveSessionState | undefined;
// Subagents inherit the parent's client env (e.g. herdr pane identity).
Expand Down Expand Up @@ -2369,7 +2377,9 @@ export class AgentDaemon {
},
},
rlmSessionDir: entry.sessionDir,
rlmDepth: entry.rlmDepth ?? 1,
rlmDepth: existsSync(entry.sessionFile)
? resolveSessionRlmDepth(sessionManager.getHeader() ?? {}, entry.sessionFile)
: undefined,
rlmMaxDepth: entry.rlmMaxDepth ?? 1,
rlmParentNodeId: entry.rlmParentNodeId ?? entry.childId,
},
Expand Down Expand Up @@ -5585,23 +5595,6 @@ function hasDaemonOutboundActiveSessionId(
return "activeSessionId" in message && typeof message.activeSessionId === "string";
}

function serializeSavedSessionInfo(session: SessionInfo): DaemonSavedSessionInfo {
return {
path: session.path,
id: session.id,
cwd: session.cwd,
name: session.name,
state: session.state,
parentSessionPath: session.parentSessionPath,
created: session.created.toISOString(),
modified: session.modified.toISOString(),
messageCount: session.messageCount,
firstMessage: session.firstMessage,
allMessagesText: session.allMessagesText,
agentStatus: session.agentStatus,
};
}

export function getChildActiveSessionStates(
sessions: ReadonlyMap<string, ActiveSessionState>,
parentState: ActiveSessionState,
Expand Down
Loading