Merge PR #8664: fix(coding-agent): preserve artifacts in CLI forks (@anatoli-tsinovoy)
This commit is contained in:
@@ -111,6 +111,7 @@ export class TanCommandController {
|
||||
let jobId = "";
|
||||
try {
|
||||
const cloneManager = await SessionManager.forkFrom(parentFile, cwd, sessionDir, undefined, {
|
||||
copyArtifacts: false,
|
||||
suppressBreadcrumb: true,
|
||||
sessionFile: cloneFile,
|
||||
});
|
||||
|
||||
@@ -328,7 +328,7 @@ import {
|
||||
SessionMaintenance,
|
||||
type SessionMaintenanceHost,
|
||||
} from "./session-maintenance";
|
||||
import { cleanupEmptyMoveSession, type SessionManager } from "./session-manager";
|
||||
import { cleanupEmptyMoveSession, copySessionArtifacts, type SessionManager } from "./session-manager";
|
||||
import { SessionMemory, type SessionMemoryHost } from "./session-memory";
|
||||
import { buildSessionMetadata } from "./session-metadata";
|
||||
import { SessionProviderBoundary, type SessionProviderBoundaryHost } from "./session-provider-boundary";
|
||||
@@ -6700,24 +6700,7 @@ export class AgentSession {
|
||||
// under a fresh id, so the work already produced is still this session's.
|
||||
this.#recovery.reanchorServedAttribution(previousSessionId);
|
||||
|
||||
// Copy artifacts directory if it exists
|
||||
const oldArtifactDir = forkResult.oldSessionFile.slice(0, -6);
|
||||
const newArtifactDir = forkResult.newSessionFile.slice(0, -6);
|
||||
|
||||
try {
|
||||
const oldDirStat = await fs.promises.stat(oldArtifactDir);
|
||||
if (oldDirStat.isDirectory()) {
|
||||
await fs.promises.cp(oldArtifactDir, newArtifactDir, { recursive: true });
|
||||
}
|
||||
} catch (err) {
|
||||
if (!isEnoent(err)) {
|
||||
logger.warn("Failed to copy artifacts during fork", {
|
||||
oldArtifactDir,
|
||||
newArtifactDir,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
await copySessionArtifacts(forkResult.oldSessionFile, forkResult.newSessionFile);
|
||||
|
||||
// Update agent session ID
|
||||
this.#freshProviderSessionId = undefined;
|
||||
|
||||
@@ -97,7 +97,31 @@ function fileSafeTimestamp(iso: string): string {
|
||||
}
|
||||
|
||||
function artifactsDirectoryFor(sessionFile: string | undefined): string | null {
|
||||
return sessionFile ? sessionFile.slice(0, -JSONL_SUFFIX_LENGTH) : null;
|
||||
if (!sessionFile?.endsWith(".jsonl")) return null;
|
||||
return sessionFile.slice(0, -JSONL_SUFFIX_LENGTH);
|
||||
}
|
||||
|
||||
/** Copy a session's artifact directory to another session, matching interactive `/fork`. */
|
||||
export async function copySessionArtifacts(sourceSessionFile: string, destinationSessionFile: string): Promise<void> {
|
||||
const sourceArtifactsDir = artifactsDirectoryFor(sourceSessionFile);
|
||||
const destinationArtifactsDir = artifactsDirectoryFor(destinationSessionFile);
|
||||
if (!sourceArtifactsDir || !destinationArtifactsDir) return;
|
||||
if (path.resolve(sourceArtifactsDir) === path.resolve(destinationArtifactsDir)) return;
|
||||
|
||||
try {
|
||||
const sourceStat = await fs.promises.stat(sourceArtifactsDir);
|
||||
if (sourceStat.isDirectory()) {
|
||||
await fs.promises.cp(sourceArtifactsDir, destinationArtifactsDir, { recursive: true });
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isEnoent(error)) {
|
||||
logger.warn("Failed to copy artifacts during fork", {
|
||||
sourceArtifactsDir,
|
||||
destinationArtifactsDir,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1440,10 +1464,13 @@ export class SessionManager {
|
||||
|
||||
const oldSessionFile = this.#sessionFile;
|
||||
const newSessionFile = path.join(nextSessionDir, path.basename(oldSessionFile));
|
||||
const oldArtifactsDir = artifactsDirectoryFor(oldSessionFile)!;
|
||||
const newArtifactsDir = artifactsDirectoryFor(newSessionFile)!;
|
||||
const oldArtifactsDir = artifactsDirectoryFor(oldSessionFile);
|
||||
const newArtifactsDir = artifactsDirectoryFor(newSessionFile);
|
||||
const sessionPathChanged = path.resolve(oldSessionFile) !== path.resolve(newSessionFile);
|
||||
const artifactPathChanged = path.resolve(oldArtifactsDir) !== path.resolve(newArtifactsDir);
|
||||
const artifactPathChanged =
|
||||
oldArtifactsDir !== null &&
|
||||
newArtifactsDir !== null &&
|
||||
path.resolve(oldArtifactsDir) !== path.resolve(newArtifactsDir);
|
||||
sessionFileExisted = this.#storage.existsSync(oldSessionFile);
|
||||
|
||||
let sessionMoved = false;
|
||||
@@ -1467,7 +1494,7 @@ export class SessionManager {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (artifactsMoved) {
|
||||
if (artifactsMoved && oldArtifactsDir && newArtifactsDir) {
|
||||
try {
|
||||
await fs.promises.rename(newArtifactsDir, oldArtifactsDir);
|
||||
} catch (rollbackErr) {
|
||||
@@ -2533,16 +2560,16 @@ export class SessionManager {
|
||||
* session file while creating a fresh session file in this sessionDir.
|
||||
*
|
||||
* `options.sessionFile` pins the new session's file path (default: an
|
||||
* auto-named `<timestamp>_<id>.jsonl` in `sessionDir`). Callers that register
|
||||
* the fork as a named agent (e.g. `/tan`) pass `<agentId>.jsonl` so the
|
||||
* persisted-subagent scan keys the agent by the same id the live ref uses.
|
||||
* auto-named `<timestamp>_<id>.jsonl` in `sessionDir`). Artifacts are copied
|
||||
* recursively by default; nested agents that deliberately share their parent's
|
||||
* artifact root may disable this with `copyArtifacts: false`.
|
||||
*/
|
||||
static async forkFrom(
|
||||
sourcePath: string,
|
||||
cwd: string,
|
||||
sessionDir?: string,
|
||||
storage: SessionStorage = new FileSessionStorage(),
|
||||
options?: { suppressBreadcrumb?: boolean; sessionFile?: string },
|
||||
options?: { copyArtifacts?: boolean; suppressBreadcrumb?: boolean; sessionFile?: string },
|
||||
): Promise<SessionManager> {
|
||||
const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
||||
const manager = new SessionManager(cwd, dir, true, storage);
|
||||
@@ -2575,6 +2602,9 @@ export class SessionManager {
|
||||
manager.sanitizeLoadedOpenAIResponsesReplayMetadata();
|
||||
manager.#forceFileCreation = true;
|
||||
await manager.#rewriteAtomically();
|
||||
if (options?.copyArtifacts !== false) {
|
||||
await copySessionArtifacts(sourcePath, manager.#sessionFile!);
|
||||
}
|
||||
return manager;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user