fix(heartbeat): preserve task_session and symlink-equal cwds in inverse-migration branch
Addresses Greptile review on this PR: - P1 (Task Sessions Lose Continuity): the inverse-of-migration branch in resolveRuntimeSessionParamsForWorkspace compared only against the fallback agent-home cwd, so a valid task_session whose resolved cwd matches the persisted cwd was still dropped and the next heartbeat opened a fresh conversation. Now short-circuits and preserves the session whenever canonicalizeCwdForComparison(previousCwd) === canonicalizeCwdForComparison(resolvedCwd). - P2 (Symlinked Home Sessions Drop): all cwd equality checks in this function now flow through canonicalizeCwdForComparison, which prefers fs.realpathSync and falls back to path.resolve when the path does not exist. A fallback-home session persisted through a symlinked path no longer compares unequal against the physical fallback dir returned by the resolver. Tests: heartbeat-workspace-session gains two new cases — task_session preservation with a real on-disk tmp cwd, and symlink-equal fallback preservation. Refs greptile-apps[bot] review comments on PR #9219.
This commit is contained in:
parent
fc200eff00
commit
a3e00b9de5
|
|
@ -1013,6 +1013,73 @@ describe("resolveRuntimeSessionParamsForWorkspace", () => {
|
|||
});
|
||||
expect(result.warning).toBeNull();
|
||||
});
|
||||
|
||||
it("preserves a task_session when the resolved cwd matches the persisted cwd", async () => {
|
||||
const agentId = "agent-123";
|
||||
// Use a real on-disk cwd so realpath resolution succeeds; the interesting
|
||||
// case is that the task_session's cwd is neither project_primary nor the
|
||||
// fallback agent home. The previous implementation dropped it wholesale.
|
||||
const taskSessionCwd = await fs.mkdtemp(path.join(os.tmpdir(), "pcp-tasksess-"));
|
||||
try {
|
||||
const result = resolveRuntimeSessionParamsForWorkspace({
|
||||
agentId,
|
||||
previousSessionParams: {
|
||||
sessionId: "task-session-abc",
|
||||
cwd: taskSessionCwd,
|
||||
workspaceId: "workspace-9",
|
||||
},
|
||||
resolvedWorkspace: buildResolvedWorkspace({
|
||||
cwd: taskSessionCwd,
|
||||
source: "task_session",
|
||||
projectId: null,
|
||||
workspaceId: "workspace-9",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.sessionParams).toEqual({
|
||||
sessionId: "task-session-abc",
|
||||
cwd: taskSessionCwd,
|
||||
workspaceId: "workspace-9",
|
||||
});
|
||||
expect(result.warning).toBeNull();
|
||||
} finally {
|
||||
await fs.rm(taskSessionCwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves a fallback-home session that was persisted via a symlink to the real fallback dir", async () => {
|
||||
const agentId = "agent-symlink";
|
||||
const realFallbackCwd = resolveDefaultAgentWorkspaceDir(agentId);
|
||||
await fs.mkdir(realFallbackCwd, { recursive: true });
|
||||
const symlinkDir = await fs.mkdtemp(path.join(os.tmpdir(), "pcp-symlink-"));
|
||||
const symlinkPath = path.join(symlinkDir, "fallback-alias");
|
||||
await fs.symlink(realFallbackCwd, symlinkPath);
|
||||
try {
|
||||
const result = resolveRuntimeSessionParamsForWorkspace({
|
||||
agentId,
|
||||
previousSessionParams: {
|
||||
sessionId: "session-symlink",
|
||||
cwd: symlinkPath, // persisted through symlinked path
|
||||
workspaceId: null,
|
||||
},
|
||||
resolvedWorkspace: buildResolvedWorkspace({
|
||||
cwd: realFallbackCwd, // resolver hands back the real fallback path
|
||||
source: "agent_home",
|
||||
projectId: null,
|
||||
workspaceId: null,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.sessionParams).toEqual({
|
||||
sessionId: "session-symlink",
|
||||
cwd: symlinkPath,
|
||||
workspaceId: null,
|
||||
});
|
||||
expect(result.warning).toBeNull();
|
||||
} finally {
|
||||
await fs.rm(symlinkDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyPersistedExecutionWorkspaceConfig", () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import fs from "node:fs/promises";
|
||||
import { realpathSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFile as execFileCallback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
|
@ -2473,6 +2474,21 @@ export function parseSessionCompactionPolicy(agent: typeof agents.$inferSelect):
|
|||
return resolveSessionCompactionPolicy(agent.adapterType, agent.runtimeConfig).policy;
|
||||
}
|
||||
|
||||
// Canonicalize a cwd for equality comparison across symlinked paths. The CLI
|
||||
// encodes session files under the physical directory it saw, so a persisted
|
||||
// cwd captured through a symlink must compare equal to the same physical
|
||||
// directory expressed as its real path. Falls back to `path.resolve` when the
|
||||
// path does not exist on disk (which is legitimate in unit tests and for
|
||||
// prospective/planned cwds).
|
||||
function canonicalizeCwdForComparison(target: string): string {
|
||||
const normalized = path.resolve(target);
|
||||
try {
|
||||
return realpathSync(normalized);
|
||||
} catch {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveRuntimeSessionParamsForWorkspace(input: {
|
||||
agentId: string;
|
||||
previousSessionParams: Record<string, unknown> | null;
|
||||
|
|
@ -2487,6 +2503,8 @@ export function resolveRuntimeSessionParamsForWorkspace(input: {
|
|||
warning: null as string | null,
|
||||
};
|
||||
}
|
||||
const canonicalPreviousCwd = canonicalizeCwdForComparison(previousCwd);
|
||||
const canonicalResolvedCwd = canonicalizeCwdForComparison(resolvedWorkspace.cwd);
|
||||
// Inverse-of-migration case: the persisted session was captured in a
|
||||
// project workspace but the current run resolves to the agent's home
|
||||
// workspace (e.g. a timer/heartbeat wake with no active issue while the
|
||||
|
|
@ -2497,8 +2515,20 @@ export function resolveRuntimeSessionParamsForWorkspace(input: {
|
|||
// and the state stays coherent (rather than accumulating stderr warnings
|
||||
// while the CLI silently self-heals).
|
||||
if (resolvedWorkspace.source !== "project_primary") {
|
||||
// Safe to resume when the resolved cwd matches the persisted cwd — the
|
||||
// session jsonl exists under the same directory. Covers the `task_session`
|
||||
// source where `resolveWorkspaceForRun` deliberately reuses the persisted
|
||||
// session's cwd, and any `agent_home` resolution that lands on the exact
|
||||
// same directory that hosted the session.
|
||||
if (canonicalPreviousCwd === canonicalResolvedCwd) {
|
||||
return {
|
||||
sessionParams: previousSessionParams,
|
||||
warning: null as string | null,
|
||||
};
|
||||
}
|
||||
const fallbackAgentHomeCwd = resolveDefaultAgentWorkspaceDir(agentId);
|
||||
if (path.resolve(previousCwd) !== path.resolve(fallbackAgentHomeCwd)) {
|
||||
const canonicalFallbackCwd = canonicalizeCwdForComparison(fallbackAgentHomeCwd);
|
||||
if (canonicalPreviousCwd !== canonicalFallbackCwd) {
|
||||
return {
|
||||
sessionParams: null,
|
||||
warning:
|
||||
|
|
@ -2520,13 +2550,14 @@ export function resolveRuntimeSessionParamsForWorkspace(input: {
|
|||
};
|
||||
}
|
||||
const fallbackAgentHomeCwd = resolveDefaultAgentWorkspaceDir(agentId);
|
||||
if (path.resolve(previousCwd) !== path.resolve(fallbackAgentHomeCwd)) {
|
||||
const canonicalFallbackCwd = canonicalizeCwdForComparison(fallbackAgentHomeCwd);
|
||||
if (canonicalPreviousCwd !== canonicalFallbackCwd) {
|
||||
return {
|
||||
sessionParams: previousSessionParams,
|
||||
warning: null as string | null,
|
||||
};
|
||||
}
|
||||
if (path.resolve(projectCwd) === path.resolve(previousCwd)) {
|
||||
if (canonicalizeCwdForComparison(projectCwd) === canonicalPreviousCwd) {
|
||||
return {
|
||||
sessionParams: previousSessionParams,
|
||||
warning: null as string | null,
|
||||
|
|
|
|||
Loading…
Reference in New Issue