mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 08:11:17 -10:00
feat(handover): 0.5.2-07 A4-profile — thread codex_config_profile through handover
The A2-1 model-fidelity fix's sibling, one field over. The codex adapter already emits `-p <profile>` from binding.codexConfigProfile and the RESTORE path already threads it; only HANDOVER dropped it — lookupNode SELECTed id/runtime/cwd/model but not codex_config_profile, so a profile-pinned codex seat reverted to no-profile at every handover. Thread it the same way as model: NodeRow + lookupNode SELECT (seat-handover-service), successor node object, SuccessorNode + transient binding (successor-session-launcher). Adapter unchanged (already consumes binding.codexConfigProfile). RED-first EFFECT proof: real lookupNode->createSuccessor->launchHarness path asserts the successor binding carries the SPEC-pinned profile (mirrors the A2-1 money proof). Enumeration guard extended to fence the profile field across the handover path. Gate: daemon tsc 0, full daemon suite 7330 passed / 0 failed (hermetic, env-less). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
bba1ad264a
commit
1e4bfee835
4 files changed
+58
-4
No files matched your search
@@ -80,6 +80,9 @@ interface NodeRow {
|
||||
// 0.5.2-07: the seat's SPEC-pinned model, threaded onto the successor binding so handover
|
||||
// does not silently revert a spec-pinned seat to the runtime default (adapter emits -m/--model).
|
||||
model: string | null;
|
||||
// 0.5.2-07 A4-profile: the seat's SPEC-pinned codex config profile (nodes.codex_config_profile),
|
||||
// threaded onto the successor binding for the same reason as model — the adapter emits `-p <profile>`.
|
||||
codex_config_profile: string | null;
|
||||
}
|
||||
|
||||
interface SessionRow {
|
||||
@@ -330,7 +333,9 @@ export class SeatHandoverService {
|
||||
// Seam B: the successor is the SAME seat continuing — persisted policy posture carries.
|
||||
// 0.5.2-07 model fidelity: carry the seat's SPEC-pinned model so the successor launch reads the
|
||||
// spec (else the running topology drifts from the founder-designed one at every handover).
|
||||
node: { id: node.id, runtime: node.runtime, cwd: node.cwd, launchPosture: successorPosture, model: node.model },
|
||||
// A4-profile: likewise carry the codex config profile (adapter emits -p) — the restore path
|
||||
// already threads it; handover must too, or a profile-pinned codex seat reverts at handover.
|
||||
node: { id: node.id, runtime: node.runtime, cwd: node.cwd, launchPosture: successorPosture, model: node.model, codexConfigProfile: node.codex_config_profile ?? undefined },
|
||||
departingSessionName: latestSession.session_name,
|
||||
occupantGeneration,
|
||||
...(predecessorGeneration
|
||||
@@ -640,7 +645,7 @@ export class SeatHandoverService {
|
||||
|
||||
private lookupNode(status: SeatStatus): NodeRow {
|
||||
return this.db.prepare(
|
||||
"SELECT id, runtime, cwd, model FROM nodes WHERE rig_id = ? AND logical_id = ?"
|
||||
"SELECT id, runtime, cwd, model, codex_config_profile FROM nodes WHERE rig_id = ? AND logical_id = ?"
|
||||
).get(status.rig_id, status.logical_id) as NodeRow;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ export interface SuccessorNode {
|
||||
* running topology drift from the founder-designed one. Populated by the caller from node provenance;
|
||||
* absent → the adapter emits no model flag (unchanged for legacy/unpinned seats). */
|
||||
model?: string | null;
|
||||
/** 0.5.2-07 A4-profile: the seat's SPEC-pinned codex config profile (nodes.codex_config_profile).
|
||||
* Same continuity rationale as model — populated by the caller from node provenance; absent → the
|
||||
* adapter emits no -p flag (unchanged for legacy/unpinned seats). */
|
||||
codexConfigProfile?: string | null;
|
||||
}
|
||||
|
||||
export type SuccessorLaunchResult =
|
||||
@@ -285,6 +289,8 @@ export class SuccessorSessionLauncher {
|
||||
...(node.launchPosture ? { launchPosture: node.launchPosture } : {}),
|
||||
// 0.5.2-07: the successor reads the seat's SPEC-pinned model (adapter emits -m/--model).
|
||||
model: node.model ?? undefined,
|
||||
// 0.5.2-07 A4-profile: the successor reads the seat's SPEC-pinned codex config profile (adapter emits -p).
|
||||
codexConfigProfile: node.codexConfigProfile ?? undefined,
|
||||
};
|
||||
|
||||
let launch: Awaited<ReturnType<RuntimeAdapter["launchHarness"]>>;
|
||||
|
||||
@@ -94,4 +94,27 @@ describe("0.5.2-07 A2-3 — model-fidelity enumeration guard", () => {
|
||||
// And it IS still used by the metadata/inventory surface (classification is live, not dead).
|
||||
expect(callers.sort()).toEqual(["node-inventory.ts", "resume-metadata-refresher.ts"]);
|
||||
});
|
||||
|
||||
// 0.5.2-07 A4-profile — extend the fence to the codex_config_profile field. The codex adapter already
|
||||
// emits `-p` from binding.codexConfigProfile; the HANDOVER path (the A2-1 fix's sibling) must SELECT
|
||||
// the column at lookupNode AND carry it onto the successor binding, exactly as it now does for model.
|
||||
// A future edit that threads the profile into the binding but forgets the SELECT (or vice-versa) fails HERE.
|
||||
it("handover threads codex_config_profile: lookupNode SELECTs it AND the successor binding carries it", () => {
|
||||
const handover = read("domain/seat-handover-service.ts");
|
||||
// The lookupNode query must SELECT the column (mirrors the A2-1 model SELECT, one field over).
|
||||
expect(handover, "seat-handover-service lookupNode must SELECT codex_config_profile").toMatch(
|
||||
/SELECT id, runtime, cwd, model, codex_config_profile FROM nodes/,
|
||||
);
|
||||
// The NodeRow shape must carry it, and createSuccessor must forward it onto the successor node.
|
||||
expect(handover, "NodeRow must carry codex_config_profile").toMatch(/codex_config_profile: string \| null/);
|
||||
expect(handover, "createSuccessor must forward the profile onto the successor node").toMatch(
|
||||
/codexConfigProfile: node\.codex_config_profile/,
|
||||
);
|
||||
// The launcher must thread it from the successor node onto the transient binding the adapter reads.
|
||||
const launcher = read("domain/successor-session-launcher.ts");
|
||||
expect(launcher, "SuccessorNode must declare codexConfigProfile").toMatch(/codexConfigProfile\?: string \| null/);
|
||||
expect(launcher, "the transient binding must thread codexConfigProfile").toMatch(
|
||||
/codexConfigProfile: node\.codexConfigProfile/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -109,9 +109,9 @@ describe("SeatHandoverService", () => {
|
||||
});
|
||||
}
|
||||
|
||||
function seedSeat(opts?: { runtime?: string; withSession?: boolean; model?: string }) {
|
||||
function seedSeat(opts?: { runtime?: string; withSession?: boolean; model?: string; codexConfigProfile?: string }) {
|
||||
const rig = rigRepo.createRig("seat-rig");
|
||||
const node = rigRepo.addNode(rig.id, "dev.impl", { runtime: opts?.runtime ?? "codex", cwd: "/project", model: opts?.model });
|
||||
const node = rigRepo.addNode(rig.id, "dev.impl", { runtime: opts?.runtime ?? "codex", cwd: "/project", model: opts?.model, codexConfigProfile: opts?.codexConfigProfile });
|
||||
let sessionId: string | null = null;
|
||||
if (opts?.withSession !== false) {
|
||||
const session = sessionRegistry.registerSession(node.id, "dev-impl@seat-rig");
|
||||
@@ -470,6 +470,26 @@ describe("SeatHandoverService", () => {
|
||||
expect(successorBinding.model).toBe("gpt-5.4-cheap");
|
||||
});
|
||||
|
||||
it("MONEY PROOF (0.5.2-07 A4-profile): a codex_config_profile-pinned seat's handover launches the successor with that profile — the REAL lookupNode→createSuccessor→launchHarness path", async () => {
|
||||
// A4-profile mirrors A2-1 for the codex_config_profile column. The codex adapter already emits
|
||||
// `-p <profile>` from binding.codexConfigProfile; the gap is the HANDOVER threading — lookupNode
|
||||
// must SELECT codex_config_profile and createSuccessor must carry it onto the successor binding.
|
||||
// RED on this base: lookupNode SELECTs only id/runtime/cwd/model, so the pinned profile is lost
|
||||
// before the successor binding is ever built — the same handover-reverts gap as the model, one
|
||||
// field over. The restore path already threads it (restore-orchestrator.ts); handover did not.
|
||||
seedSeat({ runtime: "codex", codexConfigProfile: "prod-sandboxed" });
|
||||
const result = await service.handover({
|
||||
seatRef: "dev-impl@seat-rig",
|
||||
reason: "context-wall",
|
||||
source: "fresh",
|
||||
operator: "orch-lead@seat-rig",
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
expect(launchHarness).toHaveBeenCalledTimes(1);
|
||||
const successorBinding = launchHarness.mock.calls[0]![0] as { codexConfigProfile?: string };
|
||||
expect(successorBinding.codexConfigProfile).toBe("prod-sandboxed");
|
||||
});
|
||||
|
||||
it("composes the full cycle for a fresh source: create -> deliver -> verify -> rebind", async () => {
|
||||
const { node } = seedSeat({ runtime: "codex" });
|
||||
|
||||
|
||||
Reference in new issue
Block a user