mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
Avoid session resets when reusing default task directories (#17419)
This commit is contained in:
1 parent
f7716ec65b
commit
e5c9e60c5a
4 files changed
+34
-7
No files matched your search
@@ -26,6 +26,8 @@ OpenCode and ACPX require the existing qualified provider profiles.
|
||||
Workspace-free providers are not supported here. Codex warm retention is opt-in through `warmTaskVersion: 1`; ACPX and OpenCode remain per-turn.
|
||||
The plugin owns project workspace preparation. Core does not prepare or reuse
|
||||
a Core Git worktree for a task-only environment.
|
||||
Selecting a plain task directory does not add redundant default configuration.
|
||||
Retaining that directory does not itself change the run configuration.
|
||||
For plugin-backed project storage, Core uses the workspace provider contract.
|
||||
The storage plugin must also provide the selected execution environment.
|
||||
Its declared workspace modes and strategies remain in effect.
|
||||
|
||||
@@ -159,7 +159,7 @@ describePostgres("heartbeat execution configuration through adapter dispatch", (
|
||||
config: { sourceWorkspaceConfig: { ...agent.adapterConfig, homeMode: "preconfigured" }, aiBinding: managed
|
||||
? aiRuntimeConnectionBindingSchema.parse(agent.runtimeConfig!.aiConnection) : undefined },
|
||||
policy: { trustPreset: { kind: "standard", preset: "standard", boundary: null, sourcePresets: {} },
|
||||
useIsolatedTaskDirectory: false, explicitTaskDirectory: false },
|
||||
useIsolatedTaskDirectory: false, explicitTaskDirectory: false, requestedExecutionWorkspaceMode: "shared_workspace" },
|
||||
environment: { selectedEnvironmentId: null, selectedEnvironmentForConfig: null,
|
||||
resolvedInstanceSettings: await instanceSettingsService(db).get(), persistedNativeExecutionInput: persisted },
|
||||
services: { secretsSvc: secretService(db), companySkills: companySkillService(db), appendRunEvent: vi.fn(), finalizeAiConnectionBusyDeferral: vi.fn() },
|
||||
@@ -220,7 +220,7 @@ describePostgres("heartbeat execution configuration through adapter dispatch", (
|
||||
}
|
||||
});
|
||||
|
||||
async function configuration(f: Awaited<ReturnType<typeof seed>>, config: Record<string, unknown>, snapshot: Record<string, unknown> = {}, type = "paperclip_runner") {
|
||||
async function configuration(f: Awaited<ReturnType<typeof seed>>, config: Record<string, unknown>, snapshot: Record<string, unknown> = {}, type = "paperclip_runner", explicitTaskDirectory = false, requestedExecutionWorkspaceMode: HeartbeatExecutionConfigurationInput["policy"]["requestedExecutionWorkspaceMode"] = "shared_workspace") {
|
||||
const [agent] = await db.update(agents).set({ adapterType: type, adapterConfig: config }).where(eq(agents.id, f.agentId)).returning();
|
||||
const [run] = await db.insert(heartbeatRuns).values({ companyId: f.companyId, agentId: f.agentId, issueId: f.issueId, scopeKind: "issue", status: "running", contextSnapshot: snapshot }).returning();
|
||||
const resolvedInstanceSettings = await instanceSettingsService(db).get();
|
||||
@@ -230,7 +230,7 @@ describePostgres("heartbeat execution configuration through adapter dispatch", (
|
||||
acceptedPlanContinuationWake: false, acceptedPlanWakeRoutingDecision: null,
|
||||
taskProjectContext: null, routineEnvContext: { routineId: null, env: null, responsibleUserId: null } },
|
||||
config: { sourceWorkspaceConfig: config, aiBinding: undefined },
|
||||
policy: { trustPreset: { kind: "standard", preset: "standard", boundary: null, sourcePresets: {} }, useIsolatedTaskDirectory: false, explicitTaskDirectory: false },
|
||||
policy: { trustPreset: { kind: "standard", preset: "standard", boundary: null, sourcePresets: {} }, useIsolatedTaskDirectory: false, explicitTaskDirectory, requestedExecutionWorkspaceMode },
|
||||
environment: { selectedEnvironmentId: null, selectedEnvironmentForConfig: null,
|
||||
resolvedInstanceSettings, persistedNativeExecutionInput: null },
|
||||
services: { secretsSvc: secretService(db), companySkills: companySkillService(db), appendRunEvent: vi.fn(), finalizeAiConnectionBusyDeferral: vi.fn() },
|
||||
@@ -238,6 +238,28 @@ describePostgres("heartbeat execution configuration through adapter dispatch", (
|
||||
}) };
|
||||
}
|
||||
|
||||
it("keeps default task-directory configuration stable when the directory is reused", async () => {
|
||||
const f = await seed();
|
||||
const initial = await (await configuration(f, {}, {}, "paperclip_runner", true)).prepare();
|
||||
const reused = await (await configuration(f, {})).prepare();
|
||||
expect(initial.prepared).toBe(true);
|
||||
expect(reused.prepared).toBe(true);
|
||||
if (!initial.prepared || !reused.prepared) throw new Error("Configuration was not prepared");
|
||||
expect(initial.mergedConfig).toEqual(reused.mergedConfig);
|
||||
expect(initial.configSnapshot).toEqual(reused.configSnapshot);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ mode: "shared_workspace" as const, config: { workspaceStrategy: { type: "git_worktree", baseRef: "main" } } },
|
||||
{ mode: "agent_default" as const, config: {} },
|
||||
])("still selects directory transport instead of a $mode strategy", async ({ mode, config }) => {
|
||||
const f = await seed();
|
||||
const prepared = await (await configuration(f, config, {}, "paperclip_runner", true, mode)).prepare();
|
||||
expect(prepared.prepared).toBe(true);
|
||||
if (!prepared.prepared) throw new Error("Configuration was not prepared");
|
||||
expect(prepared.mergedConfig.workspaceStrategy).toEqual({ type: "project_primary" });
|
||||
});
|
||||
|
||||
it.each(["fresh", "saved empty"])("prepares a %s native skill selection while another run holds the company cache lock", async kind => {
|
||||
const f = await seed();
|
||||
const selected = kind === "saved empty" ? await companySkillService(db).createLocalSkill(f.companyId, { name: "Later assignment" }) : null;
|
||||
|
||||
@@ -2614,7 +2614,7 @@ export function heartbeatService(
|
||||
run, agent,
|
||||
task: { issueId, context, responsibleUserId, acceptedPlanContinuationWake, acceptedPlanWakeRoutingDecision, taskProjectContext, routineEnvContext },
|
||||
config: { sourceWorkspaceConfig, aiBinding },
|
||||
policy: { trustPreset, useIsolatedTaskDirectory, explicitTaskDirectory },
|
||||
policy: { trustPreset, useIsolatedTaskDirectory, explicitTaskDirectory, requestedExecutionWorkspaceMode },
|
||||
environment: { selectedEnvironmentId, selectedEnvironmentForConfig, resolvedInstanceSettings, persistedNativeExecutionInput },
|
||||
services: { secretsSvc, companySkills, appendRunEvent, finalizeAiConnectionBusyDeferral },
|
||||
onFailureSecretsResolved: (readSecrets) => { readFailureReportSecrets = () => [...readSecrets(), ...identityRedactor.values]; },
|
||||
|
||||
@@ -68,7 +68,7 @@ export interface HeartbeatExecutionConfigurationInput {
|
||||
aiBinding: AiConnectionBinding | undefined;
|
||||
};
|
||||
policy: Pick<HeartbeatWorkspacePreparationInput["policy"],
|
||||
"trustPreset" | "useIsolatedTaskDirectory"> & { explicitTaskDirectory: boolean };
|
||||
"trustPreset" | "useIsolatedTaskDirectory" | "requestedExecutionWorkspaceMode"> & { explicitTaskDirectory: boolean };
|
||||
environment: Pick<EnvironmentSelection,
|
||||
"selectedEnvironmentId" | "selectedEnvironmentForConfig" | "resolvedInstanceSettings" | "persistedNativeExecutionInput">;
|
||||
services: {
|
||||
@@ -108,14 +108,17 @@ export async function prepareHeartbeatExecutionConfiguration(db: Db, input: Hear
|
||||
// preparation step can fail. Subsequent provider/identity values extend it.
|
||||
onFailureSecretsResolved(() => []);
|
||||
const aiBinding = preconfiguredHome ? undefined : input.config.aiBinding;
|
||||
const { trustPreset, useIsolatedTaskDirectory, explicitTaskDirectory } = input.policy;
|
||||
const { trustPreset, useIsolatedTaskDirectory, explicitTaskDirectory, requestedExecutionWorkspaceMode } = input.policy;
|
||||
const { secretsSvc, companySkills, appendRunEvent, finalizeAiConnectionBusyDeferral } = input.services;
|
||||
let managedAiRuntime: ManagedAiRuntime | undefined;
|
||||
const forceDirectoryStrategy = useIsolatedTaskDirectory || (explicitTaskDirectory && (
|
||||
requestedExecutionWorkspaceMode === "agent_default" || Object.keys(parseObject(sourceWorkspaceConfig.workspaceStrategy)).length > 0
|
||||
));
|
||||
const mergedConfig = {
|
||||
...sourceWorkspaceConfig,
|
||||
// The base below is already task-owned. Keep directory transport while
|
||||
// preserving isolated mode and the mandatory sandbox preflight.
|
||||
...((useIsolatedTaskDirectory || explicitTaskDirectory) ? { workspaceStrategy: { type: "project_primary" } } : {}),
|
||||
...(forceDirectoryStrategy ? { workspaceStrategy: { type: "project_primary" } } : {}),
|
||||
};
|
||||
const configSnapshot = buildExecutionWorkspaceConfigSnapshot(
|
||||
mergedConfig,
|
||||
|
||||
Reference in new issue
Block a user