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
59 changes: 59 additions & 0 deletions docs/specs/2026-07-31-preserve-frozen-session-workspace-binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Preserve frozen Session workspace binding

## Traceability

- Spec ID: preserve-frozen-session-workspace-binding
- Status: Implemented

## Intent

Keep the workspace qualification metadata of eligible Sessions intact when an
evidence bundle freezes its shared Session population. This prevents a Session
that was already qualified for the requested workspace from being discarded
when the Session evidence and lead lanes consume the frozen population.

## Acceptance Scenarios

- AC-1: Freezing a Session with non-enumerable workspace CWD candidates
preserves those candidates on the frozen Session without making them part of
the public enumerable or serialized contract.
- AC-2: A Codex evidence bundle whose frozen population contains one eligible
workspace-qualified Session reports one eligible and selected Session in the
Session facts lane instead of failing with
`SESSION_POPULATION_BINDING_MISMATCH`.

## Non-goals

- Changing Session discovery, active-Session omission, time-window filtering,
workspace qualification policy, or population fingerprints.
- Making workspace CWD candidates enumerable or exposing them in report output.
- Changing renderer, finding, scoring, or report schemas.

## Plan and Tasks

1. Preserve the private workspace CWD candidates while cloning each Session in
`freezeSessionPopulation`.
2. Add a focused regression test proving the candidates survive freezing and
remain non-enumerable.
3. Run the Session population tests, evidence-bundle tests, full package tests,
and a real Codex evidence-bundle collection against the reproducing
workspace.

## Test and Review Evidence

- AC-1: `node --test test/session-population.test.mjs`
- AC-2: `node --test test/better-harness-evidence-bundle.test.mjs`
- Regression gate: `npm test`
- Real-path verification: run `harness evidence-bundle` for Codex against the
reproducing workspace and require `status: complete`, an available Session
evidence lane, and a bound Session population.
- Risk review: confirm workspace CWD candidates remain absent from
`Object.keys`, object spread, and `JSON.stringify` output.

Observed evidence:

- Focused Session population, task-loop source, and evidence-bundle tests:
55 passed, 0 failed.
- Full package suite: 1019 passed, 0 failed.
- Real Codex evidence bundle: `complete`; Session and lead lanes `available`;
population binding `bound`; eligible and analyzed counts `1/1`.
5 changes: 4 additions & 1 deletion scripts/harness-analysis/task-loop-source.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { projectCheckupReportEvidence } from "../coding-agent-practices/checkup/
import { buildTaskEpisodes, stableFingerprint } from "../session-analysis/episode-contract.mjs";
import { buildObservationManifest } from "../session-analysis/observation-manifest.mjs";
import { sanitizePrivateReviewText } from "../session-analysis/privacy-safe-text.mjs";
import { cloneSessionWithWorkspaceCwds } from "../session-analysis/provider-runner.mjs";
import { sessionAnalysisRef } from "../session-analysis/session-ref.mjs";
import {
bindSessionSelection,
Expand Down Expand Up @@ -1102,7 +1103,9 @@ export async function createTaskLoopSourceFromSessions(options = {}) {
? sessionPopulationDiscovery(population)
: await analyzer.analyze({ ...analyzerOptions, command: "sources" });
const inventorySource = population?.sessions ?? discovery.sessions;
const sessionInventory = Object.freeze(inventorySource.map((session) => Object.freeze(structuredClone(session))));
const sessionInventory = Object.freeze(
inventorySource.map((session) => Object.freeze(cloneSessionWithWorkspaceCwds(session))),
);
if (selectionProfile) {
assertSessionSelectionBinding(selectionProfile, selectionPlan, { eligibleCount: sessionInventory.length });
}
Expand Down
5 changes: 5 additions & 0 deletions scripts/session-analysis/provider-runner.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export function sessionWorkspaceCwds(session) {
return typeof explicit === "string" && explicit.length > 0 ? [explicit] : [];
}

export function cloneSessionWithWorkspaceCwds(session) {
const clonedSession = structuredClone(session);
return bindSessionWorkspaceCwds(clonedSession, sessionWorkspaceCwds(session));
}

export function sessionWorkspaceCwd(session, workspaceScope) {
if (!workspaceScope) return sessionWorkspaceCwds(session)[0] ?? null;
if (session?.workspaceMatch === WORKSPACE_SESSION_MATCH.DIRECT_CWD) {
Expand Down
25 changes: 23 additions & 2 deletions scripts/session-analysis/session-population.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import {
createFactsRunContext,
prepareFactsSessionInventory,
} from "./session-core-facts.mjs";
import {
bindSessionWorkspaceCwds,
cloneSessionWithWorkspaceCwds,
sessionWorkspaceCwds,
} from "./provider-runner.mjs";

export const SESSION_POPULATION_BINDING_SCHEMA_VERSION = 1;
export const SESSION_SELECTION_BINDING_SCHEMA_VERSION = 1;
Expand Down Expand Up @@ -41,6 +46,14 @@ function bindingError(code, message) {
return Object.assign(new Error(message), { code });
}

function freezeSession(session, inheritedWorkspaceCwds = []) {
const frozenCandidate = cloneSessionWithWorkspaceCwds(session);
if (sessionWorkspaceCwds(frozenCandidate).length === 0) {
bindSessionWorkspaceCwds(frozenCandidate, inheritedWorkspaceCwds);
}
return Object.freeze(frozenCandidate);
}

export function freezeSessionPopulation({
scope = {},
sessions = [],
Expand All @@ -58,8 +71,16 @@ export function freezeSessionPopulation({
...(excludedSessionId ? { "exclude-session-id": excludedSessionId } : {}),
_factsStartedAt: startedAt,
}, platform, providerSessionId);
const prepared = prepareFactsSessionInventory(rows(sessions), factsContext);
const frozenSessions = Object.freeze(prepared.sessions.map((session) => Object.freeze(structuredClone(session))));
const sourceSessions = rows(sessions);
const workspaceCwdsBySessionId = new Map(sourceSessions.flatMap((session) => {
const sessionId = String(session?.sessionId ?? "").trim();
return sessionId ? [[sessionId, sessionWorkspaceCwds(session)]] : [];
}));
const prepared = prepareFactsSessionInventory(sourceSessions, factsContext);
const frozenSessions = Object.freeze(prepared.sessions.map((session) => {
const sessionId = String(session?.sessionId ?? "").trim();
return freezeSession(session, sessionId ? workspaceCwdsBySessionId.get(sessionId) : []);
}));
const ids = sessionIds(frozenSessions);
const population = {
sessions: frozenSessions,
Expand Down
26 changes: 26 additions & 0 deletions test/session-population.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ async function populationModule() {
return import("../scripts/session-analysis/session-population.mjs");
}

async function workspaceModule() {
return import("../scripts/session-analysis/provider-runner.mjs");
}

test("frozen population binding omits exact active and Qoder home-only sessions", async () => {
const { freezeSessionPopulation } = await populationModule();
const population = freezeSessionPopulation({
Expand Down Expand Up @@ -33,6 +37,28 @@ test("frozen population binding omits exact active and Qoder home-only sessions"
assert.doesNotMatch(JSON.stringify(population.binding), /active-private|home-private|eligible-private|\/private/u);
});

test("frozen population preserves private Session workspace CWD candidates", async () => {
const { freezeSessionPopulation } = await populationModule();
const { bindSessionWorkspaceCwds, sessionWorkspaceCwds } = await workspaceModule();
const session = bindSessionWorkspaceCwds(
{ sessionId: "eligible" },
["/workspace", "/workspace/member"],
);

const population = freezeSessionPopulation({
scope: { platform: "codex", workspace: "/workspace", until: "2026-07-30T00:00:00.000Z" },
sessions: [session],
suppliedUntil: true,
});
const [frozenSession] = population.sessions;

assert.deepEqual(sessionWorkspaceCwds(frozenSession), ["/workspace", "/workspace/member"]);
assert.deepEqual(Object.keys(frozenSession), ["sessionId"]);
assert.deepEqual({ ...frozenSession }, { sessionId: "eligible" });
assert.equal(JSON.stringify(frozenSession), "{\"sessionId\":\"eligible\"}");
assert.equal(Object.isFrozen(frozenSession), true);
});

test("selection binding rejects a session outside the frozen population", async () => {
const { bindSessionSelection, freezeSessionPopulation } = await populationModule();
const population = freezeSessionPopulation({
Expand Down
24 changes: 17 additions & 7 deletions test/task-loop-source.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import test from "node:test";

import { buildCheckupScan } from "../scripts/coding-agent-practices/checkup/scan.mjs";
import { freezeSessionPopulation } from "../scripts/session-analysis/session-population.mjs";
import {
bindSessionWorkspaceCwds,
sessionWorkspaceCwds,
} from "../scripts/session-analysis/provider-runner.mjs";
import {
buildHarnessReviewPacket,
validateHarnessReviewPacket,
Expand Down Expand Up @@ -401,13 +405,14 @@ test("requested usage reuses one frozen population without rediscovery and emits
const root = await mkdtemp(path.join(os.tmpdir(), "better-harness-frozen-usage-"));
const workspace = path.join(root, "workspace");
const calls = [];
const initialSessions = ["session-a", "session-b"].map((sessionId) => ({
sessionId,
firstSeen: "2026-07-17T08:00:00.000Z",
lastSeen: "2026-07-17T08:05:00.000Z",
sourceKinds: ["fixture"],
sourceRefs: [{ kind: "project-session", path: "/fixture/session.jsonl" }],
}));
const initialSessions = ["session-a", "session-b"].map((sessionId) =>
bindSessionWorkspaceCwds({
sessionId,
firstSeen: "2026-07-17T08:00:00.000Z",
lastSeen: "2026-07-17T08:05:00.000Z",
sourceKinds: ["fixture"],
sourceRefs: [{ kind: "project-session", path: "/fixture/session.jsonl" }],
}, [workspace]));
const activity = {
schemaVersion: 1,
dateBasis: "UTC",
Expand Down Expand Up @@ -449,6 +454,7 @@ test("requested usage reuses one frozen population without rediscovery and emits
until: options.until,
piHome: options.piHome,
inventory: options.sessionInventory?.map((session) => session.sessionId) ?? null,
workspaceCwds: options.sessionInventory?.map(sessionWorkspaceCwds) ?? null,
});
if (options.command === "sources") {
return {
Expand Down Expand Up @@ -524,6 +530,10 @@ test("requested usage reuses one frozen population without rediscovery and emits
["session-a", "session-b"],
["session-a", "session-b"],
]);
assert.deepEqual(calls.filter((call) => call.command === "insights").map((call) => call.workspaceCwds), [
[[workspace], [workspace]],
[[workspace], [workspace]],
]);
} finally {
await rm(root, { recursive: true, force: true });
}
Expand Down