diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index a8c500a4387..0e44d7feb81 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -1776,6 +1776,11 @@ preparation. Both phases keep their original ordering in `heartbeat.ts`; they acquire no runtime resources. Public-path characterization tests live in `server/src/__tests__/heartbeat-session-preparation.test.ts`. +Session compatibility uses effective workspace policy and settings. Generic issue +and project row timestamps do not reset a session. This includes the first +project-owned directory binding. Changes to effective execution configuration +still require a fresh session. + Run-log formatting is in `server/src/services/heartbeat/run-log.ts`. It bounds stored event payloads, redacts and shortens log chunks, and caps stdout/stderr excerpts. `run-output.ts` owns per-attempt log handles, current-user redaction, diff --git a/server/src/__tests__/heartbeat-workspace-session.test.ts b/server/src/__tests__/heartbeat-workspace-session.test.ts index 5df8621bbb6..154a7ff0600 100644 --- a/server/src/__tests__/heartbeat-workspace-session.test.ts +++ b/server/src/__tests__/heartbeat-workspace-session.test.ts @@ -2427,6 +2427,57 @@ describe("effective run session config freshness", () => { }); }); + it.each([null, "2026-06-01T00:00:00.000Z"])("does not reset for project row timestamps (%s) but still resets for project policy", async (revision) => { + const base = await buildSessionConfigMetadata({ + workspaceConfig: { + requestedMode: "agent_default", + effectiveMode: "agent_default", + projectConfigRevisionAt: revision, + projectPolicy: null, + }, + }); + const projectTouched = await buildSessionConfigMetadata({ + workspaceConfig: { + requestedMode: "agent_default", + effectiveMode: "agent_default", + projectConfigRevisionAt: "2026-06-01T00:05:00.000Z", + projectPolicy: null, + }, + }); + const policyChanged = await buildSessionConfigMetadata({ + workspaceConfig: { + requestedMode: "agent_default", + effectiveMode: "agent_default", + projectConfigRevisionAt: "2026-06-01T00:05:00.000Z", + projectPolicy: { enabled: true, defaultMode: "isolated_workspace" }, + }, + }); + + expect( + resolveTaskSessionConfigFreshness({ + hasTaskSession: true, + configuredModel: "gpt-5.4-mini", + taskSessionParams: sessionParamsWithConfigMetadata(base), + configMetadata: projectTouched, + }), + ).toMatchObject({ + reset: false, + changedCategories: [], + reasons: [], + }); + expect( + resolveTaskSessionConfigFreshness({ + hasTaskSession: true, + configuredModel: "gpt-5.4-mini", + taskSessionParams: sessionParamsWithConfigMetadata(base), + configMetadata: policyChanged, + }), + ).toMatchObject({ + reset: true, + changedCategories: ["workspaceConfig"], + }); + }); + it("does not reset when a reusable execution workspace becomes realized", async () => { const base = await buildSessionConfigMetadata({ workspaceConfig: { diff --git a/server/src/services/heartbeat/workspaces.ts b/server/src/services/heartbeat/workspaces.ts index c607e2426e3..6b036657a8c 100644 --- a/server/src/services/heartbeat/workspaces.ts +++ b/server/src/services/heartbeat/workspaces.ts @@ -2800,6 +2800,7 @@ function buildSessionConfigCategoryValues(input: { // the timestamp here makes every comment invalidate an otherwise reusable // task session. delete workspaceConfig.issueConfigRevisionAt; + delete workspaceConfig.projectConfigRevisionAt; // This row is runtime state, not requested configuration. It is absent // before the first reusable run is realized and present on the next turn; // fingerprinting that transition would rotate the native session exactly