revert: unbuild the W4 compaction-enforcement suite + three audit tails

Founder-ruled unbuild from the over-engineering audit (qitem-20260811020356-64ecb896),
DB disposition ruled at lock level (qitem-20260811020528-369f2e40).

1. W4 compaction-enforcement suite, FULL — commits 56858cbea, d789fbe51, 34fe36d55,
   07879b10b, 89fd15bf5, d4236f5bd, df05193e4. Removes the compaction-control CLI, the 3
   /api/compaction/control routes, enforcer-decision-store.ts, the human_hold skip reason,
   authorize_ttl_minutes (config key + env + settings), and the generic beforeSend hook.
   Grounds: a hold/authorize permission system for a control no human ever issued; the
   founding incident was later ruled NO-FAULT.
2. W1 guard-tail trims — the MF5 caller-supplied wake-intent-* rejection on
   POST /api/queue/outbox/record and its case-variant broadening (one block). W1's
   transactional closure core, drain, indeterminate handling and crash-boundary tests stay.
3. Gate-verdict containment tail, PARTIAL — keeps the plain outside-the-worktree typo
   guard; drops the tracked-destination, tracked-path-traversal, tracked-symlink-leaf and
   .git administrative-path legs plus their fixtures.
4. W2b poison-retry — reverts bba1ad264. The envelope mechanism (d685eb713) stays.

Technique: each touched file was classified by whether its only commits since 56858cbea^
were the 7 named W4 commits. 12 W4-only files were restored wholesale to their pre-W4
content; 4 MIXED files got surgical edits. This matters because a68a156bb (ACTIVITY 5b)
also touches session-transport.ts, startup.ts, server.ts and test-app.ts — a blind
revert-to-pre-W4 would have silently deleted in-flight ACTIVITY work, including the
mapPaneState export. That work is preserved.

DB, per the lock-level ruling: migration 068 is APPLIED history and its file STAYS, inert.
New forward migration 071 drops enforcer_decisions. DROP TABLE IF EXISTS is load-bearing —
068 is excluded from the shared full-test DB fixture, so 071 runs there against a table
that was never created.

Two consequences named rather than buried:
- Removing the MF5 route guard restores a forgeable wake-intent path by design (the only
  caller is the daemon's own localhost client). The exact-case drain selector is now the
  ONLY thing keeping a recorded WAKE-INTENT- case variant out of the executable drain; that
  W1 pin is kept and its now-false comment corrected.
- canonicalizeDestination in gate-lane.mjs is retained: it normalizes macOS /var ->
  /private/var, so its justification needs no adversary, only an OS. Removing it made 6 of 7
  gate-lane tests fail on honest paths.

Verified at base 1e4bfee83: daemon 7268 passed / 0 failed, cli 2260 passed env-less,
test:repo 164 pass / 0 fail, gate-lane binding 7/7, tsc clean in both packages. Isolated
daemon boots clean on a migrated copy of the live DB (/healthz ok, reconcile errors=0);
the 3 control routes 404 while /state and /trigger stay; rig compaction-control is an
unknown command. DB proof ran on a sqlite3 .backup copy — the live DB was never touched.

Not reviewed yet: handed to dev-guard as qitem-20260811033226-683d25ad for
accept-to-commit. Branch-only, not reachable from main, never pushed.

Guard round 1 (dev-guard NOT-CLEAR on bda641883, verdict sha256 8eeeabc0…c2cec) —
two bounded corrections applied, no scope change:

HIGH-1: startup.ts kept a W4-only `compactionSettingsStore` local, introduced by
56858cbea solely so W4 could share the settings store with EnforcerDecisionStore.
With that store deleted the local was single-use, so it left a second construction
shape for no behaviour and contradicted the FULL W4 unbuild claim. The construction
block is now byte-identical to 56858cbea^; all later non-W4 startup work retained.

HIGH-2: four records still asserted the deleted route reservation exists —
outbox-handler.ts:3-10 (prefix doc), outbox-handler.ts:281-290 (exact-case selector
rationale), queue-repository.ts:691-694 (missing-target refusal), and the
queue-transactional-closure describe label. Reworded to state the real effects:
the prefix is the executable drain namespace and NOT a route reservation, exact-case
selection is now the only thing keeping a recorded case variant out of the drain,
and the missing-target refusal stands on its own. No deleted guard was re-added and
no behaviour changed.
This commit is contained in:
v-openrig-build committed 2026-08-11 03:48:05 +00:00
1 parent 9f80644524
commit 60ee1e57be
34 files changed
+124 -3480

No files matched your search

@@ -1,152 +0,0 @@
import { Command } from "commander";
import { DaemonClient, terminalAuthHeaders } from "../client.js";
import { getDaemonStatus, getDaemonUrl, type LifecycleDeps } from "../daemon-lifecycle.js";
import { realDeps } from "./daemon.js";
export interface CompactionControlDeps {
lifecycleDeps: LifecycleDeps;
clientFactory: (url: string) => Pick<DaemonClient, "get" | "post">;
}
function defaultDeps(): CompactionControlDeps {
return {
lifecycleDeps: realDeps(),
clientFactory: (url: string) => new DaemonClient(url),
};
}
async function getClient(deps: CompactionControlDeps) {
const status = await getDaemonStatus(deps.lifecycleDeps);
if (status.state !== "running" || status.healthy === false) {
console.error("Daemon is not running. Start it with: rig daemon start");
process.exitCode = 1;
return null;
}
return deps.clientFactory(getDaemonUrl(status));
}
function printResponse(
response: { status: number; data: unknown },
json: boolean | undefined,
success: string,
): void {
if (json) {
console.log(JSON.stringify(response.data));
} else if (response.status < 400) {
console.log(success);
} else {
const data = response.data as { error?: string; message?: string };
console.error(data.message ?? data.error ?? `Request failed (HTTP ${response.status})`);
}
if (response.status >= 400) process.exitCode = response.status >= 500 ? 2 : 1;
}
export function compactionControlCommand(depsOverride?: CompactionControlDeps): Command {
const command = new Command("compaction-control")
.description("Create, inspect, and clear generation-scoped Claude compaction decisions");
const deps = () => depsOverride ?? defaultDeps();
command.addCommand(
new Command("hold")
.argument("<session>", "Target Claude session")
.requiredOption("--reason <text>", "Why automatic compaction must not act")
.option("--json", "JSON output for agents")
.action(async (session: string, options: { reason: string; json?: boolean }) => {
const client = await getClient(deps());
if (!client) return;
const response = await client.post("/api/compaction/control", {
session,
direction: "hold",
reason: options.reason,
}, { headers: terminalAuthHeaders() });
printResponse(response, options.json, `Compaction hold created for ${session}.`);
}),
);
command.addCommand(
new Command("authorize")
.argument("<session>", "Target Claude session")
.requiredOption(
"--automatic-reason <reason>",
"One refusal to lift: disabled, post_restore_cooldown, or stale_generation",
)
.requiredOption("--reason <text>", "Why this one attempt is authorized")
.option("--json", "JSON output for agents")
.action(async (
session: string,
options: { automaticReason: string; reason: string; json?: boolean },
) => {
const client = await getClient(deps());
if (!client) return;
const response = await client.post("/api/compaction/control", {
session,
direction: "authorize",
automaticReason: options.automaticReason,
reason: options.reason,
}, { headers: terminalAuthHeaders() });
printResponse(response, options.json, `One compaction attempt authorized for ${session}.`);
}),
);
command.addCommand(
new Command("list")
.option("--session <session>", "Limit decisions to one session")
.option("--json", "JSON output for agents")
.action(async (options: { session?: string; json?: boolean }) => {
const client = await getClient(deps());
if (!client) return;
const query = options.session ? `?session=${encodeURIComponent(options.session)}` : "";
const response = await client.get(`/api/compaction/control${query}`, {
headers: terminalAuthHeaders(),
});
if (options.json || response.status >= 400) {
printResponse(response, options.json, "Compaction decisions listed.");
return;
}
const data = response.data as {
decisions?: Array<{
decisionId?: string;
sessionName?: string;
direction?: string;
active?: boolean;
lastObservedAt?: string | null;
lastObservedOutcome?: string | null;
}>;
};
const decisions = data.decisions ?? [];
if (decisions.length === 0) {
console.log("No compaction decisions.");
return;
}
for (const decision of decisions) {
console.log([
decision.decisionId ?? "unknown-id",
decision.sessionName ?? "unknown-session",
decision.direction ?? "unknown-direction",
decision.active === false ? "inactive" : "active",
`lastObservedAt=${decision.lastObservedAt ?? "never"}`,
`lastObservedOutcome=${decision.lastObservedOutcome ?? "none"}`,
].join(" "));
}
}),
);
command.addCommand(
new Command("clear")
.argument("<decision-id>", "Decision ID")
.requiredOption("--reason <text>", "Why the decision is being cleared")
.option("--json", "JSON output for agents")
.action(async (decisionId: string, options: { reason: string; json?: boolean }) => {
const client = await getClient(deps());
if (!client) return;
const response = await client.post(
`/api/compaction/control/${encodeURIComponent(decisionId)}/clear`,
{ reason: options.reason },
{ headers: terminalAuthHeaders() },
);
printResponse(response, options.json, `Compaction decision ${decisionId} cleared.`);
}),
);
return command;
}
+1 -2
View File
@@ -72,8 +72,7 @@ Keys:
workflow.* exception_routing (orchestrator | human_only — the maturity-dial host default)
policies.claude_compaction.*
enabled, threshold_percent, compact_instruction,
message_inline, message_file_path,
policies.claude_compaction.authorize_ttl_minutes
message_inline, message_file_path
policies.idle_gate_qitem.scan_interval_seconds
policies.idle_gate_qitem.active_wake_interval_seconds
snapshots.periodic.* enabled, interval_seconds, retention_keep
-19
View File
@@ -116,7 +116,6 @@ export interface RiggedConfig {
messageInline: string;
messageFilePath: string;
postRestoreAuditInstruction: string;
authorizeTtlMinutes: number;
};
idleGateQitem: {
scanIntervalSeconds: number;
@@ -234,7 +233,6 @@ const DEFAULTS = {
messageInline: DEFAULT_CLAUDE_COMPACTION_RESTORE_INSTRUCTION,
messageFilePath: DEFAULT_CLAUDE_COMPACTION_EXTRA_INSTRUCTION_FILE_PATH,
postRestoreAuditInstruction: DEFAULT_CLAUDE_COMPACTION_POST_RESTORE_AUDIT_INSTRUCTION,
authorizeTtlMinutes: 15,
},
idleGateQitem: {
scanIntervalSeconds: 60,
@@ -328,7 +326,6 @@ export const VALID_KEYS = [
"policies.claude_compaction.message_inline",
"policies.claude_compaction.message_file_path",
"policies.claude_compaction.post_restore_audit_instruction",
"policies.claude_compaction.authorize_ttl_minutes",
"policies.idle_gate_qitem.scan_interval_seconds",
"policies.idle_gate_qitem.active_wake_interval_seconds",
"snapshots.periodic.enabled",
@@ -400,7 +397,6 @@ export const ENV_MAP: Record<ValidKey, { primary: string; legacy?: string }> = {
"policies.claude_compaction.message_inline": { primary: "OPENRIG_POLICIES_CLAUDE_COMPACTION_MESSAGE_INLINE" },
"policies.claude_compaction.message_file_path": { primary: "OPENRIG_POLICIES_CLAUDE_COMPACTION_MESSAGE_FILE_PATH" },
"policies.claude_compaction.post_restore_audit_instruction": { primary: "OPENRIG_POLICIES_CLAUDE_COMPACTION_POST_RESTORE_AUDIT_INSTRUCTION" },
"policies.claude_compaction.authorize_ttl_minutes": { primary: "OPENRIG_POLICIES_CLAUDE_COMPACTION_AUTHORIZE_TTL_MINUTES" },
"policies.idle_gate_qitem.scan_interval_seconds": { primary: "OPENRIG_POLICIES_IDLE_GATE_QITEM_SCAN_INTERVAL_SECONDS" },
"policies.idle_gate_qitem.active_wake_interval_seconds": { primary: "OPENRIG_POLICIES_IDLE_GATE_QITEM_ACTIVE_WAKE_INTERVAL_SECONDS" },
"snapshots.periodic.enabled": { primary: "OPENRIG_SNAPSHOTS_PERIODIC_ENABLED" },
@@ -460,7 +456,6 @@ const KEY_TO_PATH: Record<ValidKey, string[]> = {
"policies.claude_compaction.message_inline": ["policies", "claudeCompaction", "messageInline"],
"policies.claude_compaction.message_file_path": ["policies", "claudeCompaction", "messageFilePath"],
"policies.claude_compaction.post_restore_audit_instruction": ["policies", "claudeCompaction", "postRestoreAuditInstruction"],
"policies.claude_compaction.authorize_ttl_minutes": ["policies", "claudeCompaction", "authorizeTtlMinutes"],
"policies.idle_gate_qitem.scan_interval_seconds": ["policies", "idleGateQitem", "scanIntervalSeconds"],
"policies.idle_gate_qitem.active_wake_interval_seconds": ["policies", "idleGateQitem", "activeWakeIntervalSeconds"],
"snapshots.periodic.enabled": ["snapshots", "periodic", "enabled"],
@@ -640,19 +635,6 @@ const KEY_CONSTRAINTS: Partial<Record<ValidKey, (raw: string, coerced: string |
);
}
},
"policies.claude_compaction.authorize_ttl_minutes": (raw, coerced) => {
const trimmed = (raw ?? "").trim();
if (!/^-?\d+$/.test(trimmed) || typeof coerced !== "number" || !Number.isInteger(coerced)) {
throw new Error(
`Invalid value for policies.claude_compaction.authorize_ttl_minutes: expected an integer in [1, 60], got "${raw}"`,
);
}
if (coerced < 1 || coerced > 60) {
throw new Error(
`Invalid value for policies.claude_compaction.authorize_ttl_minutes: must be in [1, 60], got ${coerced}`,
);
}
},
"snapshots.periodic.interval_seconds": (raw, coerced) => {
if (!/^-?\d+$/.test((raw ?? "").trim())) {
throw new Error(
@@ -857,7 +839,6 @@ export class ConfigStore {
messageInline: v("policies.claude_compaction.message_inline") as string,
messageFilePath: v("policies.claude_compaction.message_file_path") as string,
postRestoreAuditInstruction: v("policies.claude_compaction.post_restore_audit_instruction") as string,
authorizeTtlMinutes: v("policies.claude_compaction.authorize_ttl_minutes") as number,
},
idleGateQitem: {
scanIntervalSeconds: v("policies.idle_gate_qitem.scan_interval_seconds") as number,
-6
View File
@@ -77,10 +77,6 @@ import { restoreCheckCommand } from "./commands/restore-check.js";
import { restorePacketCommand, type RestorePacketDeps } from "./commands/restore-packet.js";
import { compactPlanCommand, type CompactPlanDeps } from "./commands/compact-plan.js";
import { compactCommand, type CompactDeps } from "./commands/compact.js";
import {
compactionControlCommand,
type CompactionControlDeps,
} from "./commands/compaction-control.js";
import { heartbeatCommand, type HeartbeatDeps } from "./commands/heartbeat.js";
import { seatCommand, handoverCommand, type SeatDeps } from "./commands/seat.js";
import { rigPolicyCommand, type RigPolicyDeps } from "./commands/rig-policy.js";
@@ -148,7 +144,6 @@ export interface ProgramDeps {
restorePacketDeps?: RestorePacketDeps;
compactPlanDeps?: CompactPlanDeps;
compactDeps?: CompactDeps;
compactionControlDeps?: CompactionControlDeps;
heartbeatDeps?: HeartbeatDeps;
seatDeps?: SeatDeps;
rigPolicyDeps?: RigPolicyDeps;
@@ -240,7 +235,6 @@ export function createProgram(depsOverride?: ProgramDeps): Command {
program.addCommand(restorePacketCommand(depsOverride?.restorePacketDeps));
program.addCommand(compactPlanCommand(depsOverride?.compactPlanDeps));
program.addCommand(compactCommand(depsOverride?.compactDeps));
program.addCommand(compactionControlCommand(depsOverride?.compactionControlDeps));
program.addCommand(heartbeatCommand(depsOverride?.heartbeatDeps));
program.addCommand(seatCommand(depsOverride?.seatDeps));
program.addCommand(handoverCommand(depsOverride?.seatDeps));
@@ -143,7 +143,6 @@ describe("ConfigStore — extended namespaces (User Settings v0)", () => {
"policies.claude_compaction.message_inline",
"policies.claude_compaction.message_file_path",
"policies.claude_compaction.post_restore_audit_instruction",
"policies.claude_compaction.authorize_ttl_minutes",
// OPR.0.5.1 51-06 W2c — tunable watchdog auto-registration cadence.
"policies.idle_gate_qitem.scan_interval_seconds",
"policies.idle_gate_qitem.active_wake_interval_seconds",
-112
View File
@@ -6,7 +6,6 @@ import http from "node:http";
import { Command } from "commander";
import { ConfigStore, VALID_KEYS } from "../src/config-store.js";
import { configCommand } from "../src/commands/config.js";
import { SETTINGS_VALID_KEYS } from "../../daemon/src/domain/user-settings/settings-store.js";
import { DaemonClient } from "../src/client.js";
import { STATE_FILE, type DaemonState } from "../src/daemon-lifecycle.js";
import type { StatusDeps } from "../src/commands/status.js";
@@ -427,114 +426,3 @@ describe("Config CLI", () => {
expect(combined).toContain("policies.idle_gate_qitem.active_wake_interval_seconds");
});
});
describe("W4 compaction authorization TTL config", () => {
const key = "policies.claude_compaction.authorize_ttl_minutes";
const envKey = "OPENRIG_POLICIES_CLAUDE_COMPACTION_AUTHORIZE_TTL_MINUTES";
let tmpDir: string;
let configPath: string;
let savedEnv: string | undefined;
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), "w4-config-test-"));
configPath = join(tmpDir, "config.json");
savedEnv = process.env[envKey];
delete process.env[envKey];
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
if (savedEnv === undefined) delete process.env[envKey];
else process.env[envKey] = savedEnv;
});
it("keeps the daemon and CLI compaction key registries exactly aligned", () => {
const compactionKeys = (keys: readonly string[]) =>
keys.filter((candidate) => candidate.startsWith("policies.claude_compaction."));
expect(compactionKeys(VALID_KEYS)).toEqual(compactionKeys(SETTINGS_VALID_KEYS));
expect(VALID_KEYS).toContain(key);
});
it("defaults to 15 and exposes the typed policy field", () => {
const store = new ConfigStore(configPath);
expect(store.resolveWithSource(key)).toEqual({
value: 15,
source: "default",
defaultValue: 15,
});
expect(store.resolve().policies.claudeCompaction.authorizeTtlMinutes).toBe(15);
});
it("accepts a valid environment override", () => {
process.env[envKey] = "30";
expect(new ConfigStore(configPath).resolveWithSource(key)).toEqual({
value: 30,
source: "env",
defaultValue: 15,
});
});
it.each(["0", "61", "15.5", "bad"])(
"invalid env %s falls back to 15 and reports loudly",
(raw) => {
process.env[envKey] = raw;
const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
try {
expect(new ConfigStore(configPath).resolveWithSource(key)).toEqual({
value: 15,
source: "default",
defaultValue: 15,
});
expect(stderr.mock.calls.some((call) => String(call[0]).includes(
`env override for ${key} rejected`,
))).toBe(true);
} finally {
stderr.mockRestore();
}
},
);
it.each([0, 61, 15.5, "bad"])(
"invalid file value %s falls back to 15 and reports loudly",
(raw) => {
writeFileSync(configPath, JSON.stringify({
policies: { claudeCompaction: { authorizeTtlMinutes: raw } },
}));
const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
try {
expect(new ConfigStore(configPath).resolveWithSource(key)).toEqual({
value: 15,
source: "default",
defaultValue: 15,
});
expect(stderr.mock.calls.some((call) => String(call[0]).includes(
`file value for ${key} rejected`,
))).toBe(true);
} finally {
stderr.mockRestore();
}
},
);
it("accepts exactly 1..60 through set and rejects values outside the range", () => {
const store = new ConfigStore(configPath);
for (const value of ["1", "15", "60"]) {
store.set(key, value);
expect(store.get(key)).toBe(Number(value));
}
for (const value of ["0", "61", "15.5", "bad"]) {
expect(() => store.set(key, value)).toThrow(/integer|\[1, 60\]|number/i);
}
});
it("prints the literal key in rig config help", () => {
const command = configCommand(configPath);
let output = "";
command.configureOutput({
writeOut: (text) => { output += text; },
writeErr: () => {},
});
command.outputHelp();
expect(output).toContain(key);
});
});
@@ -1,179 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Command } from "commander";
import { createProgram } from "../src/index.js";
const COMMAND_MODULE = ["../src/commands", "compaction-control.js"].join("/");
vi.mock("../src/daemon-lifecycle.js", async () => {
const actual = await vi.importActual<Record<string, unknown>>("../src/daemon-lifecycle.js");
return {
...actual,
getDaemonStatus: vi.fn(async () => ({ state: "running", healthy: true, pid: 1234, port: 7433 })),
getDaemonUrl: vi.fn(() => "http://localhost:7433"),
};
});
interface TestDeps {
lifecycleDeps: Record<string, never>;
clientFactory: () => {
get(path: string): Promise<{ status: number; data: unknown }>;
post(path: string, body: unknown): Promise<{ status: number; data: unknown }>;
};
}
type CommandFactory = (deps: TestDeps) => Command;
async function loadCommand(): Promise<CommandFactory> {
let loaded: { compactionControlCommand?: CommandFactory } | null = null;
try {
loaded = await import(/* @vite-ignore */ COMMAND_MODULE);
} catch {
// Assertion below keeps the RED feature-shaped instead of surfacing a loader error.
}
expect(loaded, "compaction-control command module must exist").not.toBeNull();
expect(loaded?.compactionControlCommand).toBeTypeOf("function");
return loaded!.compactionControlCommand!;
}
function makeDeps() {
const calls: Array<{ method: string; path: string; body?: unknown }> = [];
const deps: TestDeps = {
lifecycleDeps: {},
clientFactory: () => ({
get: vi.fn(async (path: string) => {
calls.push({ method: "GET", path });
return {
status: 200,
data: {
ok: true,
decisions: [{
decisionId: "hold-1",
sessionName: "claude-seat@rig",
direction: "hold",
lastObservedAt: "2026-08-08T18:00:30.000Z",
lastObservedOutcome: "human_hold",
}],
},
};
}),
post: vi.fn(async (path: string, body: unknown) => {
calls.push({ method: "POST", path, body });
return { status: path.endsWith("/clear") ? 200 : 201, data: { ok: true, decisionId: "decision-1" } };
}),
}),
};
return { calls, deps };
}
describe("rig compaction-control", () => {
let logs: string[];
beforeEach(() => {
logs = [];
vi.spyOn(console, "log").mockImplementation((...args) => logs.push(args.join(" ")));
vi.spyOn(console, "error").mockImplementation(() => {});
process.exitCode = undefined;
});
it("is registered as one grouped command with hold, authorize, list, and clear", () => {
const program = createProgram({ compactionControlDeps: makeDeps().deps } as never);
const command = program.commands.find((candidate) => candidate.name() === "compaction-control");
expect(command).toBeDefined();
expect(command!.commands.map((child) => child.name()).sort()).toEqual([
"authorize",
"clear",
"hold",
"list",
]);
});
it("hold posts the target and required human reason without a body actor", async () => {
const factory = await loadCommand();
const { calls, deps } = makeDeps();
const command = factory(deps);
await command.parseAsync([
"node", "rig", "hold", "claude-seat@rig",
"--reason", "finish the atomic action",
"--json",
]);
expect(calls).toEqual([{
method: "POST",
path: "/api/compaction/control",
body: {
session: "claude-seat@rig",
direction: "hold",
reason: "finish the atomic action",
},
}]);
});
it("authorize posts exactly one named lift and no caller-controlled expiry", async () => {
const factory = await loadCommand();
const { calls, deps } = makeDeps();
const command = factory(deps);
await command.parseAsync([
"node", "rig", "authorize", "claude-seat@rig",
"--automatic-reason", "disabled",
"--reason", "allow one attempt",
"--json",
]);
expect(calls).toEqual([{
method: "POST",
path: "/api/compaction/control",
body: {
session: "claude-seat@rig",
direction: "authorize",
automaticReason: "disabled",
reason: "allow one attempt",
},
}]);
});
it("list uses the scoped read route and renders durable hold observation", async () => {
const factory = await loadCommand();
const { calls, deps } = makeDeps();
const command = factory(deps);
await command.parseAsync([
"node", "rig", "list", "--session", "claude-seat@rig", "--json",
]);
expect(calls).toEqual([{
method: "GET",
path: "/api/compaction/control?session=claude-seat%40rig",
}]);
expect(JSON.parse(logs.join(""))).toMatchObject({
decisions: [{
decisionId: "hold-1",
lastObservedAt: "2026-08-08T18:00:30.000Z",
lastObservedOutcome: "human_hold",
}],
});
});
it("human list output exposes decision identity and durable observation fields", async () => {
const factory = await loadCommand();
const { deps } = makeDeps();
const command = factory(deps);
await command.parseAsync([
"node", "rig", "list", "--session", "claude-seat@rig",
]);
const rendered = logs.join("\n");
expect(rendered).toContain("hold-1");
expect(rendered).toContain("claude-seat@rig");
expect(rendered).toContain("2026-08-08T18:00:30.000Z");
expect(rendered).toContain("human_hold");
});
it("clear records a human reason through the decision-specific route", async () => {
const factory = await loadCommand();
const { calls, deps } = makeDeps();
const command = factory(deps);
await command.parseAsync([
"node", "rig", "clear", "hold-1", "--reason", "atomic action completed", "--json",
]);
expect(calls).toEqual([{
method: "POST",
path: "/api/compaction/control/hold-1/clear",
body: { reason: "atomic action completed" },
}]);
});
});
+3 -1
View File
@@ -73,9 +73,10 @@ import { i3IdentityProvenanceSchema } from "./migrations/067_i3_identity_provena
import { enforcerDecisionsSchema } from "./migrations/068_enforcer_decisions.js";
import { appliedLaunchObservationsSchema } from "./migrations/069_applied_launch_observations.js";
import { appliedLaunchObservationInvalidationsSchema } from "./migrations/070_applied_launch_observation_invalidations.js";
import { dropEnforcerDecisionsSchema } from "./migrations/071_drop_enforcer_decisions.js";
import type { Migration } from "./migrate.js";
/** Ordered 001→070 (068 enforcer decisions, 069/070 W3 launch truth). */
/** Ordered 001→071 (068 enforcer decisions, dropped forward by 071; 069/070 W3 launch truth). */
export const ALL_MIGRATIONS: Migration[] = [
coreSchema,
bindingsSessionsSchema,
@@ -147,4 +148,5 @@ export const ALL_MIGRATIONS: Migration[] = [
enforcerDecisionsSchema,
appliedLaunchObservationsSchema,
appliedLaunchObservationInvalidationsSchema,
dropEnforcerDecisionsSchema,
];
@@ -0,0 +1,19 @@
import type { Migration } from "../migrate.js";
// Unbuild of the W4 compaction-enforcement suite (founder-ruled, over-engineering audit).
//
// FORWARD-ONLY BY RULING: 068_enforcer_decisions is APPLIED on live DBs and 069/070 sit above it,
// so its migration file stays as history and is NOT deleted — removing it would leave an applied
// ledger row with no migration and two migrations above the gap. This drops the table instead, so
// schema_migrations keeps an honest record of both the create and the drop.
//
// IF EXISTS is load-bearing, not defensive noise: 068 is excluded from the shared full-test DB
// fixture (test-app.ts migrationsForFullTestDbExclusions), so on that fixture this migration runs
// against a database where enforcer_decisions was never created. The indexes are dropped with the
// table by SQLite.
export const dropEnforcerDecisionsSchema: Migration = {
name: "071_drop_enforcer_decisions.sql",
sql: `
DROP TABLE IF EXISTS enforcer_decisions;
`,
};
@@ -1,10 +1,5 @@
import type { SendOpts, SendResult, SessionTransport } from "./session-transport.js";
import type { SessionTransport } from "./session-transport.js";
import type { SettingsStore } from "./user-settings/settings-store.js";
import type {
AuthorizableCompactionReason,
EnforcerDecision,
EnforcerDecisionStore,
} from "./enforcer-decision-store.js";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
@@ -73,8 +68,8 @@ export interface EnforcerInput {
}
export type EnforcerOutcome =
| { triggered: true; decisionId?: string; liftedReason?: AuthorizableCompactionReason }
| { triggered: false; reason: EnforcerSkipReason; decisionId?: string };
| { triggered: true }
| { triggered: false; reason: EnforcerSkipReason };
/**
* OPR.0.4.3.14 — manual compaction trigger surfaced stages (AC-3). `preparing`
@@ -102,7 +97,7 @@ export interface ManualCompactionStatus {
export type ManualCompactionOutcome =
| { triggered: true; stage: "compact-sent" }
| { triggered: false; stage: "skipped-or-failed"; reason: string; decisionId?: string };
| { triggered: false; stage: "skipped-or-failed"; reason: string };
export type EnforcerSkipReason =
| "runtime_filter"
@@ -114,8 +109,7 @@ export type EnforcerSkipReason =
| "post_restore_cooldown"
| "send_failed"
| "invalid_policy"
| "stale_generation"
| "human_hold";
| "stale_generation";
function buildCompactCommand(compactInstruction: string): string {
const normalized = compactInstruction.trim().replace(/\s+/g, " ");
@@ -294,11 +288,6 @@ function buildPostCompactTurnBoundaryPrompt(): string {
type PendingPostCompactStage = "turn_boundary" | "restore_prompt" | "compliance_prompt";
type PendingPreCompactStage = "prep_prompt_sent";
type PendingStageAuthorization = {
decisionId: string;
liftedReason: "stale_generation";
generationUuid: string;
};
export class ClaudeCompactionEnforcer {
private readonly settingsStore: SettingsStore;
@@ -322,40 +311,12 @@ export class ClaudeCompactionEnforcer {
// unknown). At drain we compare it to the LIVE generation; a mismatch = a successor inheriting a
// retired-generation stage → refuse. Injected resolver (atom-B's currentOccupantTenure by session).
private readonly pendingStageGeneration = new Map<string, string | null>();
private readonly pendingStageAuthorization = new Map<string, PendingStageAuthorization>();
// Monotonic per-seat invalidation identity. Unlike the occupant state maps, this survives
// invalidation so an already-awaiting manual trigger can detect a same-name handover even when
// best-effort successor tenure minting failed and the durable generation still appears equal.
private readonly occupantInvalidationEpoch = new Map<string, number>();
private readonly resolveOccupantGeneration?: (sessionName: string) => string | null;
private readonly decisionStore?: Pick<
EnforcerDecisionStore,
| "findActiveHold"
| "findMatchingAuthorization"
| "observeHold"
| "consumeAuthorizationForAttempt"
| "recordAuthorizationAttempt"
>;
constructor(
settingsStore: SettingsStore,
sessionTransport: SessionTransport,
opts?: {
dedupWindowMs?: number;
openrigHome?: string;
postCompactRestoreCooldownMs?: number;
manualPrepWaitMs?: number;
postCompactSendWaitMs?: number;
resolveOccupantGeneration?: (sessionName: string) => string | null;
decisionStore?: Pick<
EnforcerDecisionStore,
| "findActiveHold"
| "findMatchingAuthorization"
| "observeHold"
| "consumeAuthorizationForAttempt"
| "recordAuthorizationAttempt"
>;
},
opts?: { dedupWindowMs?: number; openrigHome?: string; postCompactRestoreCooldownMs?: number; manualPrepWaitMs?: number; postCompactSendWaitMs?: number; resolveOccupantGeneration?: (sessionName: string) => string | null },
) {
this.settingsStore = settingsStore;
this.sessionTransport = sessionTransport;
@@ -365,7 +326,6 @@ export class ClaudeCompactionEnforcer {
this.manualPrepWaitMs = opts?.manualPrepWaitMs ?? MANUAL_PREP_WAIT_MS_DEFAULT;
this.postCompactSendWaitMs = opts?.postCompactSendWaitMs ?? POST_COMPACT_SEND_WAIT_MS_DEFAULT;
this.resolveOccupantGeneration = opts?.resolveOccupantGeneration;
this.decisionStore = opts?.decisionStore;
}
/**
@@ -381,128 +341,6 @@ export class ClaudeCompactionEnforcer {
return { triggered: false, reason: "no_usage_data" };
}
const liveGenerationUuid = this.resolveOccupantGeneration?.(input.sessionName) ?? null;
const activeHold = this.findAndObserveHold(input.sessionName, liveGenerationUuid);
if (activeHold) {
return {
triggered: false,
reason: "human_hold",
decisionId: activeHold.decisionId,
};
}
let authorization: {
decisionId: string;
liftedReason: AuthorizableCompactionReason;
generationUuid: string | null;
alreadyConsumed: boolean;
} | null = null;
const lift = (automaticReason: AuthorizableCompactionReason): boolean => {
const found = this.decisionStore?.findMatchingAuthorization({
enforcerKind: "claude_compaction",
sessionName: input.sessionName,
liveGenerationUuid,
automaticReason,
}) as EnforcerDecision | null | undefined;
if (!found) return false;
authorization = {
decisionId: found.decisionId,
liftedReason: automaticReason,
generationUuid: found.generationUuid ?? liveGenerationUuid,
alreadyConsumed: false,
};
return true;
};
const sendAttempt = async (
message: string,
opts?: SendOpts,
authorizationOptions: {
consume?: boolean;
finalize?: boolean;
} = {},
): Promise<{ ok: boolean; result: SendResult | null; outcome: EnforcerOutcome }> => {
const lifted = authorization as {
decisionId: string;
liftedReason: AuthorizableCompactionReason;
generationUuid: string | null;
alreadyConsumed: boolean;
} | null;
let consumed = lifted?.alreadyConsumed ?? false;
if (lifted && (authorizationOptions.consume ?? true) && !consumed) {
consumed = this.decisionStore?.consumeAuthorizationForAttempt({
decisionId: lifted.decisionId,
enforcerKind: "claude_compaction",
liftedReason: lifted.liftedReason,
}) ?? false;
if (!consumed) {
return {
ok: false,
result: null,
outcome: { triggered: false, reason: lifted.liftedReason },
};
}
if (lifted.liftedReason === "stale_generation" && lifted.generationUuid) {
this.pendingStageAuthorization.set(input.sessionName, {
decisionId: lifted.decisionId,
liftedReason: lifted.liftedReason,
generationUuid: lifted.generationUuid,
});
}
}
let result: SendResult;
try {
result = opts === undefined
? await this.sessionTransport.send(input.sessionName, message)
: await this.sessionTransport.send(input.sessionName, message, opts);
} catch (error) {
if (!lifted) throw error;
if (consumed) {
this.decisionStore?.recordAuthorizationAttempt({
decisionId: lifted.decisionId,
outcome: "failed",
failureReason: error instanceof Error ? error.message : String(error),
});
if (lifted.liftedReason === "stale_generation") {
this.pendingStageAuthorization.delete(input.sessionName);
}
}
return {
ok: false,
result: null,
outcome: { triggered: false, reason: "send_failed" },
};
}
if (lifted && consumed && (!result.ok || (authorizationOptions.finalize ?? true))) {
this.decisionStore?.recordAuthorizationAttempt({
decisionId: lifted.decisionId,
outcome: result.ok ? "succeeded" : "failed",
...(result.ok ? {} : { failureReason: result.reason ?? result.error ?? "send_failed" }),
});
}
if (!result.ok) {
if (lifted?.liftedReason === "stale_generation" && consumed) {
this.pendingStageAuthorization.delete(input.sessionName);
}
return {
ok: false,
result,
outcome: { triggered: false, reason: "send_failed" },
};
}
return {
ok: true,
result,
outcome: lifted
? {
triggered: true,
decisionId: lifted.decisionId,
liftedReason: lifted.liftedReason,
}
: { triggered: true },
};
};
const policy = this.settingsStore.resolveClaudeCompactionPolicy();
// Defense in depth: the CLI + daemon set() paths reject invalid
// threshold values, but a hand-edited ~/.openrig/config.json could
@@ -533,7 +371,7 @@ export class ClaudeCompactionEnforcer {
// generations residue is covered by fix (b)'s generation gate (layered defense). Interpretation
// surfaced in the handoff for the PM evidence read (veto there if the literal reading was meant).
if (!policy.enabled && this.manualCompactionState.get(input.sessionName)?.operatorInitiated !== true) {
if (!lift("disabled")) return { triggered: false, reason: "disabled" };
return { triggered: false, reason: "disabled" };
}
// GHOST-STAGE (b): gen-scoped stages. A stage minted by a RETIRED occupant generation must be
// undeliverable to the successor. Compare the queue-time generation to the LIVE one. NOTE-2: an
@@ -544,37 +382,30 @@ export class ClaudeCompactionEnforcer {
if (stageGen != null) {
const liveGen = this.resolveOccupantGeneration?.(input.sessionName) ?? null;
if (liveGen != null && liveGen !== stageGen) {
const carried = this.pendingStageAuthorization.get(input.sessionName);
if (carried?.generationUuid === liveGen) {
authorization = { ...carried, alreadyConsumed: true };
} else {
this.pendingStageAuthorization.delete(input.sessionName);
}
if (!authorization && !lift("stale_generation")) {
this.invalidateOccupant(input.sessionName); // drop the retired-generation ghost stage
return { triggered: false, reason: "stale_generation" };
}
this.invalidateOccupant(input.sessionName); // drop the retired-generation ghost stage
return { triggered: false, reason: "stale_generation" };
}
}
const pendingStage = this.pendingPostCompactRestore.get(input.sessionName);
if (pendingStage === "turn_boundary") {
const boundary = await sendAttempt(
const boundary = await this.sessionTransport.send(
input.sessionName,
buildPostCompactTurnBoundaryPrompt(),
{ waitForIdleMs: this.postCompactSendWaitMs },
{ finalize: false },
);
if (!boundary.ok) {
// Busy/never-idle → no delivery, no advance; the SAME stage retries next tick.
return boundary.outcome;
return { triggered: false, reason: "send_failed" };
}
this.pendingPostCompactRestore.set(input.sessionName, "restore_prompt");
return boundary.outcome;
return { triggered: true };
}
if (pendingStage === "restore_prompt") {
// OPR.0.4.1.09: resolve the extra FOR THIS SEAT (per-seat preferred; the legacy
// global is refused if it declares a different seat) - never inject wrong-seat state.
const extra = resolvePostCompactExtra(input.sessionName, this.openrigHome, policy.messageFilePath);
const restore = await sendAttempt(
const restore = await this.sessionTransport.send(
input.sessionName,
buildPostCompactRestorePrompt({
sessionName: input.sessionName,
openrigHome: this.openrigHome,
@@ -585,32 +416,31 @@ export class ClaudeCompactionEnforcer {
ignoredWrongSeatExtra: extra.ignoredWrongSeat,
}),
{ waitForIdleMs: this.postCompactSendWaitMs },
{ finalize: false },
);
if (!restore.ok) {
// Restore is exact-once + operator-authorized: if the seat is still busy
// (mid-compaction/boundary), do NOT advance to restore-sent on an
// undelivered send — retry the SAME stage next tick.
return restore.outcome;
return { triggered: false, reason: "send_failed" };
}
this.pendingPostCompactRestore.set(input.sessionName, "compliance_prompt");
// OPR.0.4.3.14 — surface manual-trigger progress (no-op for auto seats).
this.advanceManualStage(input.sessionName, "compact-sent", "restore-sent");
return restore.outcome;
return { triggered: true };
}
if (pendingStage === "compliance_prompt") {
const compliance = await sendAttempt(
const compliance = await this.sessionTransport.send(
input.sessionName,
buildPostCompactCompliancePrompt(policy.postRestoreAuditInstruction),
{ waitForIdleMs: this.postCompactSendWaitMs },
);
if (!compliance.ok) {
// Audit cannot overtake restore: only advances once the restore turn is
// idle and this send delivers; a busy tick retries the SAME stage.
return compliance.outcome;
return { triggered: false, reason: "send_failed" };
}
this.pendingPostCompactRestore.delete(input.sessionName);
this.pendingStageGeneration.delete(input.sessionName); // GHOST-STAGE (b): stage completed → drop its gen
this.pendingStageAuthorization.delete(input.sessionName);
this.postCompactRestoreCooldownUntil.set(
input.sessionName,
Date.now() + this.postCompactRestoreCooldownMs,
@@ -618,11 +448,10 @@ export class ClaudeCompactionEnforcer {
this.triggeredAboveThreshold.delete(input.sessionName);
// OPR.0.4.3.14 — terminal manual-trigger stage (no-op for auto seats).
this.advanceManualStage(input.sessionName, "restore-sent", "audit-sent");
return compliance.outcome;
return { triggered: true };
}
this.triggeredAboveThreshold.delete(input.sessionName);
this.pendingPreCompactPrep.delete(input.sessionName);
this.pendingStageAuthorization.delete(input.sessionName);
return { triggered: false, reason: "below_threshold" };
}
@@ -636,19 +465,16 @@ export class ClaudeCompactionEnforcer {
// constant policy), while a manual trigger's restore/audit half can finish
// via this single shared path even when auto-compaction is disabled.
if (!policy.enabled) {
if (!lift("disabled")) return { triggered: false, reason: "disabled" };
return { triggered: false, reason: "disabled" };
}
const now = Date.now();
const postRestoreCooldownUntil = this.postCompactRestoreCooldownUntil.get(input.sessionName);
if (postRestoreCooldownUntil !== undefined) {
if (now < postRestoreCooldownUntil) {
if (!lift("post_restore_cooldown")) {
return { triggered: false, reason: "post_restore_cooldown" };
}
} else {
this.postCompactRestoreCooldownUntil.delete(input.sessionName);
return { triggered: false, reason: "post_restore_cooldown" };
}
this.postCompactRestoreCooldownUntil.delete(input.sessionName);
}
const last = this.lastAutoCompactAt.get(input.sessionName);
@@ -661,36 +487,35 @@ export class ClaudeCompactionEnforcer {
const preCompactStage = this.pendingPreCompactPrep.get(input.sessionName);
if (preCompactStage === undefined) {
const prep = await sendAttempt(
const prep = await this.sessionTransport.send(
input.sessionName,
buildPreCompactPrepPrompt({
usedPercentage: input.usedPercentage,
thresholdPercent: policy.thresholdPercent,
preCompactInstruction: policy.preCompactInstruction,
}),
undefined,
{ consume: false },
);
if (!prep.ok) {
return prep.outcome;
return { triggered: false, reason: "send_failed" };
}
this.pendingPreCompactPrep.set(input.sessionName, "prep_prompt_sent");
return prep.outcome;
return { triggered: true };
}
const attempt = await sendAttempt(
const result = await this.sessionTransport.send(
input.sessionName,
buildCompactCommand(policy.compactInstruction),
);
if (!attempt.ok) {
return attempt.outcome;
if (!result.ok) {
return { triggered: false, reason: "send_failed" };
}
this.lastAutoCompactAt.set(input.sessionName, now);
this.triggeredAboveThreshold.add(input.sessionName);
this.pendingPreCompactPrep.delete(input.sessionName);
this.pendingPostCompactRestore.set(input.sessionName, "turn_boundary");
this.pendingStageAuthorization.delete(input.sessionName);
// GHOST-STAGE (b): capture the occupant generation at queue time (or null when unknown).
this.pendingStageGeneration.set(input.sessionName, this.resolveOccupantGeneration?.(input.sessionName) ?? null);
return attempt.outcome;
return { triggered: true };
}
/**
@@ -714,23 +539,8 @@ export class ClaudeCompactionEnforcer {
*/
async triggerManualCompact(
input: EnforcerInput,
opts: {
operatorInitiated?: boolean;
resolveCurrentSessionName?: (sessionName: string) => string | null;
} = {},
opts: { operatorInitiated?: boolean } = {},
): Promise<ManualCompactionOutcome> {
const startingInvalidationEpoch = this.occupantInvalidationEpoch.get(input.sessionName) ?? 0;
const startingGenerationUuid = this.resolveOccupantGeneration?.(input.sessionName) ?? null;
const activeHold = this.findAndObserveHold(input.sessionName, startingGenerationUuid);
if (activeHold) {
return {
triggered: false,
stage: "skipped-or-failed",
reason: "human_hold",
decisionId: activeHold.decisionId,
};
}
// OPR.0.4.3.14 rev1-r2 fix — SAME-SEAT IN-PROGRESS GUARD (race-safe), at the
// VERY TOP before ANY recordManualFailure path. This synchronous check-and-set
// runs BEFORE the first await; because JS is run-to-completion, two concurrent
@@ -788,88 +598,19 @@ export class ClaudeCompactionEnforcer {
}),
);
if (!prep.ok) {
return this.recordManualFailure(
input.sessionName,
prep.reason ?? "send_failed",
undefined,
startingInvalidationEpoch,
);
return this.recordManualFailure(input.sessionName, prep.reason ?? "send_failed");
}
// Phase 2 — WAIT for the prep turn to complete (seat idle), THEN send
// /compact. `waitForIdleMs` makes the transport block on explicit idle
// evidence before pasting /compact, guaranteeing prep-before-compact.
const preCompactCheck = {
reason: null as string | null,
decisionId: undefined as string | undefined,
};
const compact = await this.sessionTransport.send(
input.sessionName,
buildCompactCommand(policy.compactInstruction),
{
waitForIdleMs: this.manualPrepWaitMs,
beforeSend: () => {
const currentSessionName = opts.resolveCurrentSessionName
? opts.resolveCurrentSessionName(input.sessionName)
: input.sessionName;
if (!currentSessionName) {
preCompactCheck.reason = "session_not_current";
return {
reason: preCompactCheck.reason,
error: `Session '${input.sessionName}' is no longer current.`,
};
}
const currentGenerationUuid = this.resolveOccupantGeneration?.(currentSessionName) ?? null;
const currentHold = this.findAndObserveHold(currentSessionName, currentGenerationUuid);
if (currentHold) {
preCompactCheck.reason = "human_hold";
preCompactCheck.decisionId = currentHold.decisionId;
return {
reason: preCompactCheck.reason,
error: `Session '${currentSessionName}' has an active human compaction hold.`,
};
}
if (currentSessionName !== input.sessionName) {
preCompactCheck.reason = "session_not_current";
return {
reason: preCompactCheck.reason,
error: `Session '${input.sessionName}' is no longer current; current session is '${currentSessionName}'.`,
};
}
if (currentGenerationUuid !== startingGenerationUuid) {
preCompactCheck.reason = "occupant_generation_changed";
return {
reason: preCompactCheck.reason,
error: `Session '${input.sessionName}' changed occupant generation during manual compaction preparation.`,
};
}
if ((this.occupantInvalidationEpoch.get(input.sessionName) ?? 0) !== startingInvalidationEpoch) {
preCompactCheck.reason = "occupant_generation_changed";
return {
reason: preCompactCheck.reason,
error: `Session '${input.sessionName}' was invalidated during manual compaction preparation.`,
};
}
return null;
},
},
{ waitForIdleMs: this.manualPrepWaitMs },
);
if (!compact.ok) {
if (preCompactCheck.reason) {
return this.recordManualFailure(
input.sessionName,
preCompactCheck.reason,
preCompactCheck.decisionId,
startingInvalidationEpoch,
);
}
return this.recordManualFailure(
input.sessionName,
compact.reason ?? "send_failed",
undefined,
startingInvalidationEpoch,
);
return this.recordManualFailure(input.sessionName, compact.reason ?? "send_failed");
}
// Seed the EXISTING post-compact back-half (turn_boundary → restore_prompt
@@ -891,7 +632,6 @@ export class ClaudeCompactionEnforcer {
this.triggeredAboveThreshold.add(input.sessionName);
this.pendingPreCompactPrep.delete(input.sessionName);
this.pendingPostCompactRestore.set(input.sessionName, "turn_boundary");
this.pendingStageAuthorization.delete(input.sessionName);
// GHOST-STAGE (b): capture the occupant generation at queue time (or null when unknown).
this.pendingStageGeneration.set(input.sessionName, this.resolveOccupantGeneration?.(input.sessionName) ?? null);
this.setManualStage(input.sessionName, "compact-sent", undefined, opts.operatorInitiated === true);
@@ -912,10 +652,6 @@ export class ClaudeCompactionEnforcer {
* record. Occupant-scoped (no atom-B): the retiring occupant is gone, so a name match IS the ghost.
*/
invalidateOccupant(sessionName: string): void {
this.occupantInvalidationEpoch.set(
sessionName,
(this.occupantInvalidationEpoch.get(sessionName) ?? 0) + 1,
);
this.lastAutoCompactAt.delete(sessionName);
this.postCompactRestoreCooldownUntil.delete(sessionName);
this.triggeredAboveThreshold.delete(sessionName);
@@ -923,49 +659,15 @@ export class ClaudeCompactionEnforcer {
this.pendingPostCompactRestore.delete(sessionName);
this.manualCompactionState.delete(sessionName);
this.pendingStageGeneration.delete(sessionName); // GHOST-STAGE (b): drop the captured queue-time gen
this.pendingStageAuthorization.delete(sessionName);
}
private setManualStage(sessionName: string, stage: ManualCompactionStage, reason?: string, operatorInitiated?: boolean): void {
this.manualCompactionState.set(sessionName, { stage, reason, updatedAt: Date.now(), operatorInitiated });
}
private recordManualFailure(
sessionName: string,
reason: string,
decisionId?: string,
expectedInvalidationEpoch?: number,
): ManualCompactionOutcome {
if (
expectedInvalidationEpoch === undefined
|| (this.occupantInvalidationEpoch.get(sessionName) ?? 0) === expectedInvalidationEpoch
) {
this.setManualStage(sessionName, "skipped-or-failed", reason);
}
return {
triggered: false,
stage: "skipped-or-failed",
reason,
...(decisionId ? { decisionId } : {}),
};
}
private findAndObserveHold(
sessionName: string,
liveGenerationUuid: string | null,
): EnforcerDecision | null {
const hold = this.decisionStore?.findActiveHold({
enforcerKind: "claude_compaction",
sessionName,
liveGenerationUuid,
}) ?? null;
if (hold) {
this.decisionStore?.observeHold({
decisionId: hold.decisionId,
outcome: "human_hold",
});
}
return hold;
private recordManualFailure(sessionName: string, reason: string): ManualCompactionOutcome {
this.setManualStage(sessionName, "skipped-or-failed", reason);
return { triggered: false, stage: "skipped-or-failed", reason };
}
/**
@@ -1,359 +0,0 @@
import type Database from "better-sqlite3";
import { randomUUID } from "node:crypto";
export const CLAUDE_COMPACTION_ENFORCER_KIND = "claude_compaction" as const;
export const AUTHORIZABLE_COMPACTION_REASONS = [
"disabled",
"post_restore_cooldown",
"stale_generation",
] as const;
export type AuthorizableCompactionReason = typeof AUTHORIZABLE_COMPACTION_REASONS[number];
export type EnforcerDecisionDirection = "hold" | "authorize";
export interface EnforcerDecision {
decisionId: string;
enforcerKind: string;
sessionName: string;
generationUuid: string;
direction: EnforcerDecisionDirection;
automaticReason: string | null;
reason: string;
actorSession: string;
identityProvenance: string;
createdAt: string;
expiresAt: string | null;
active: boolean;
releaseKind: string | null;
releasedAt: string | null;
releasedBySession: string | null;
releaseIdentityProvenance: string | null;
releaseReason: string | null;
consumedAt: string | null;
consumedByEnforcerKind: string | null;
liftedReason: string | null;
attemptOutcome: string | null;
attemptFailureReason: string | null;
lastObservedAt: string | null;
lastObservedOutcome: string | null;
}
export interface CreateEnforcerDecisionInput {
enforcerKind: string;
sessionName: string;
generationUuid: string;
direction: EnforcerDecisionDirection;
automaticReason?: string | null;
reason: string;
actorSession: string;
identityProvenance: string;
}
interface DecisionRow {
decision_id: string;
enforcer_kind: string;
session_name: string;
generation_uuid: string;
direction: EnforcerDecisionDirection;
automatic_reason: string | null;
reason: string;
actor_session: string;
identity_provenance: string;
created_at: string;
expires_at: string | null;
active: number;
release_kind: string | null;
released_at: string | null;
released_by_session: string | null;
release_identity_provenance: string | null;
release_reason: string | null;
consumed_at: string | null;
consumed_by_enforcer_kind: string | null;
lifted_reason: string | null;
attempt_outcome: string | null;
attempt_failure_reason: string | null;
last_observed_at: string | null;
last_observed_outcome: string | null;
}
const AUTHORIZABLE = new Set<string>(AUTHORIZABLE_COMPACTION_REASONS);
function required(value: unknown, field: string): string {
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error(`${field} is required`);
}
return value.trim();
}
function mapDecision(row: DecisionRow): EnforcerDecision {
return {
decisionId: row.decision_id,
enforcerKind: row.enforcer_kind,
sessionName: row.session_name,
generationUuid: row.generation_uuid,
direction: row.direction,
automaticReason: row.automatic_reason,
reason: row.reason,
actorSession: row.actor_session,
identityProvenance: row.identity_provenance,
createdAt: row.created_at,
expiresAt: row.expires_at,
active: row.active === 1,
releaseKind: row.release_kind,
releasedAt: row.released_at,
releasedBySession: row.released_by_session,
releaseIdentityProvenance: row.release_identity_provenance,
releaseReason: row.release_reason,
consumedAt: row.consumed_at,
consumedByEnforcerKind: row.consumed_by_enforcer_kind,
liftedReason: row.lifted_reason,
attemptOutcome: row.attempt_outcome,
attemptFailureReason: row.attempt_failure_reason,
lastObservedAt: row.last_observed_at,
lastObservedOutcome: row.last_observed_outcome,
};
}
export class EnforcerDecisionStore {
constructor(
private readonly db: Database.Database,
private readonly opts: { now: () => Date; authorizeTtlMinutes: () => number },
) {}
private nowIso(): string {
return this.opts.now().toISOString();
}
private expireAuthorizations(now = this.nowIso()): void {
this.db.prepare(`
UPDATE enforcer_decisions
SET active = 0,
release_kind = 'expired',
released_at = ?
WHERE active = 1
AND direction = 'authorize'
AND expires_at IS NOT NULL
AND expires_at <= ?
`).run(now, now);
}
private getById(decisionId: string): EnforcerDecision | null {
const row = this.db.prepare(
"SELECT * FROM enforcer_decisions WHERE decision_id = ?",
).get(decisionId) as DecisionRow | undefined;
return row ? mapDecision(row) : null;
}
create(input: CreateEnforcerDecisionInput): EnforcerDecision {
const enforcerKind = required(input.enforcerKind, "enforcer kind");
const sessionName = required(input.sessionName, "session name");
const generationUuid = required(input.generationUuid, "generation UUID");
const reason = required(input.reason, "reason");
const actorSession = required(input.actorSession, "actor session");
const identityProvenance = required(input.identityProvenance, "identity provenance");
if (input.direction !== "hold" && input.direction !== "authorize") {
throw new Error("direction must be hold or authorize");
}
let automaticReason: string | null = null;
if (input.direction === "authorize") {
automaticReason = required(input.automaticReason, "automatic reason");
if (!AUTHORIZABLE.has(automaticReason)) {
throw new Error(`automatic reason '${automaticReason}' is not authorizable`);
}
}
const now = this.nowIso();
this.expireAuthorizations(now);
const decisionId = randomUUID();
const ttlMinutes = this.opts.authorizeTtlMinutes();
const expiresAt = input.direction === "authorize"
? new Date(this.opts.now().getTime() + ttlMinutes * 60_000).toISOString()
: null;
const transaction = this.db.transaction(() => {
const active = this.db.prepare(`
SELECT direction FROM enforcer_decisions
WHERE enforcer_kind = ? AND session_name = ? AND generation_uuid = ? AND active = 1
`).get(enforcerKind, sessionName, generationUuid) as { direction: EnforcerDecisionDirection } | undefined;
if (input.direction === "authorize" && active?.direction === "hold") {
throw new Error("cannot authorize while an active hold exists");
}
if (active?.direction === input.direction) {
throw new Error(`an active ${input.direction} decision already exists`);
}
if (input.direction === "hold" && active?.direction === "authorize") {
this.db.prepare(`
UPDATE enforcer_decisions
SET active = 0,
release_kind = 'revoked_by_hold',
released_at = ?,
released_by_session = ?,
release_identity_provenance = ?,
release_reason = ?
WHERE enforcer_kind = ? AND session_name = ? AND generation_uuid = ? AND active = 1
`).run(
now,
actorSession,
identityProvenance,
reason,
enforcerKind,
sessionName,
generationUuid,
);
}
this.db.prepare(`
INSERT INTO enforcer_decisions (
decision_id, enforcer_kind, session_name, generation_uuid, direction,
automatic_reason, reason, actor_session, identity_provenance,
created_at, expires_at, active
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
`).run(
decisionId,
enforcerKind,
sessionName,
generationUuid,
input.direction,
automaticReason,
reason,
actorSession,
identityProvenance,
now,
expiresAt,
);
});
transaction.immediate();
return this.getById(decisionId)!;
}
list(input: { sessionName?: string } = {}): EnforcerDecision[] {
this.expireAuthorizations();
const rows = input.sessionName
? this.db.prepare(`
SELECT * FROM enforcer_decisions WHERE session_name = ?
ORDER BY created_at DESC, rowid DESC
`).all(input.sessionName) as DecisionRow[]
: this.db.prepare(`
SELECT * FROM enforcer_decisions ORDER BY created_at DESC, rowid DESC
`).all() as DecisionRow[];
return rows.map(mapDecision);
}
clear(input: {
decisionId: string;
actorSession: string;
identityProvenance: string;
reason: string;
}): EnforcerDecision {
const decisionId = required(input.decisionId, "decision ID");
const actorSession = required(input.actorSession, "actor session");
const identityProvenance = required(input.identityProvenance, "identity provenance");
const reason = required(input.reason, "reason");
const result = this.db.prepare(`
UPDATE enforcer_decisions
SET active = 0,
release_kind = 'cleared',
released_at = ?,
released_by_session = ?,
release_identity_provenance = ?,
release_reason = ?
WHERE decision_id = ? AND active = 1
`).run(this.nowIso(), actorSession, identityProvenance, reason, decisionId);
if (result.changes !== 1) throw new Error("active decision not found");
return this.getById(decisionId)!;
}
findActiveHold(input: {
enforcerKind: string;
sessionName: string;
liveGenerationUuid: string | null;
}): EnforcerDecision | null {
this.expireAuthorizations();
const row = input.liveGenerationUuid === null
? this.db.prepare(`
SELECT * FROM enforcer_decisions
WHERE enforcer_kind = ? AND session_name = ? AND direction = 'hold' AND active = 1
ORDER BY created_at DESC, rowid DESC LIMIT 1
`).get(input.enforcerKind, input.sessionName) as DecisionRow | undefined
: this.db.prepare(`
SELECT * FROM enforcer_decisions
WHERE enforcer_kind = ? AND session_name = ? AND generation_uuid = ?
AND direction = 'hold' AND active = 1
LIMIT 1
`).get(input.enforcerKind, input.sessionName, input.liveGenerationUuid) as DecisionRow | undefined;
return row ? mapDecision(row) : null;
}
findMatchingAuthorization(input: {
enforcerKind: string;
sessionName: string;
liveGenerationUuid: string | null;
automaticReason: string;
}): EnforcerDecision | null {
this.expireAuthorizations();
if (input.liveGenerationUuid === null) return null;
const row = this.db.prepare(`
SELECT * FROM enforcer_decisions
WHERE enforcer_kind = ? AND session_name = ? AND generation_uuid = ?
AND direction = 'authorize' AND automatic_reason = ? AND active = 1
LIMIT 1
`).get(
input.enforcerKind,
input.sessionName,
input.liveGenerationUuid,
input.automaticReason,
) as DecisionRow | undefined;
return row ? mapDecision(row) : null;
}
observeHold(input: { decisionId: string; outcome: "human_hold" }): void {
this.db.prepare(`
UPDATE enforcer_decisions
SET last_observed_at = ?, last_observed_outcome = ?
WHERE decision_id = ? AND direction = 'hold' AND active = 1
`).run(this.nowIso(), input.outcome, input.decisionId);
}
consumeAuthorizationForAttempt(input: {
decisionId: string;
enforcerKind: string;
liftedReason: string;
}): boolean {
const now = this.nowIso();
this.expireAuthorizations(now);
const result = this.db.prepare(`
UPDATE enforcer_decisions
SET active = 0,
consumed_at = ?,
consumed_by_enforcer_kind = ?,
lifted_reason = ?
WHERE decision_id = ?
AND direction = 'authorize'
AND automatic_reason = ?
AND active = 1
AND expires_at > ?
`).run(
now,
input.enforcerKind,
input.liftedReason,
input.decisionId,
input.liftedReason,
now,
);
return result.changes === 1;
}
recordAuthorizationAttempt(input: {
decisionId: string;
outcome: "succeeded" | "failed";
failureReason?: string;
}): void {
this.db.prepare(`
UPDATE enforcer_decisions
SET attempt_outcome = ?, attempt_failure_reason = ?
WHERE decision_id = ? AND consumed_at IS NOT NULL
`).run(input.outcome, input.failureReason ?? null, input.decisionId);
}
}
+5 -5
View File
@@ -206,17 +206,17 @@ export class EventBus {
? "invalid event payload JSON"
: "invalid event payload shape";
const payloadSha = createHash("sha256").update(row.payload).digest("hex");
this.notifyDrainStatus = {
state: "unparseable",
watermark: row.seq,
lastPoison: { seq: row.seq, error, payloadSha },
};
this.persistWithinTransaction({
type: "event.delivery_poisoned",
poisonedSeq: row.seq,
error,
payloadSha,
});
this.notifyDrainStatus = {
state: "unparseable",
watermark: row.seq,
lastPoison: { seq: row.seq, error, payloadSha },
};
cursor = row.seq;
continue;
}
+16 -12
View File
@@ -1,13 +1,16 @@
import type Database from "better-sqlite3";
/**
* MF5: the RESERVED, executable wake-intent namespace. A durable wake intent is
* written by the daemon (QueueRepository.stageWakeIntent) with an id under this
* prefix, and the startup drain EXECUTES every pending row under it as a real
* wake. Callers of the public `/outbox/record` audit route are therefore forbidden
* from writing an id under this prefix — otherwise an ordinary audit write could
* forge a startup wake. Single source of truth for the reservation (route) and the
* drain (query).
* The EXECUTABLE wake-intent namespace. A durable wake intent is written by the
* daemon (QueueRepository.stageWakeIntent) with an id under this prefix, and the
* startup drain EXECUTES every pending row under it as a real wake.
*
* NOT a route reservation: the public `/outbox/record` audit route no longer
* refuses caller-supplied ids under this prefix (the W4-era MF5 guard was unbuilt —
* founder ruling, over-engineering audit: its justification required an adversary
* inside this trust domain, where the only caller is the daemon's own localhost
* client). A caller CAN now record an id under this prefix and the drain will
* select it. Single source of truth for the drain query only.
*/
export const WAKE_INTENT_PREFIX = "wake-intent-";
@@ -282,11 +285,12 @@ export class OutboxHandler {
* so a caller can page and terminate on a served short batch (never a silent cap).
*/
listPending(idPrefix: string, limit = 200): OutboxEntry[] {
// BLOCKING 2 (guard re-seal): EXACT-CASE prefix match. SQLite `LIKE` is
// case-insensitive by default, so a `LIKE 'wake-intent-%'` selector would
// execute a `WAKE-INTENT-…` variant that the case-sensitive route reservation
// passes — a forgeable gap. `substr(...) = ?` uses the binary collation
// (case-sensitive), aligning the executable selector with the reservation.
// EXACT-CASE prefix match. SQLite `LIKE` is case-insensitive by default, so a
// `LIKE 'wake-intent-%'` selector would also execute `WAKE-INTENT-…` variants.
// `substr(...) = ?` uses the binary collation (case-sensitive), so exactly one
// spelling is executable. With the route-side prefix refusal unbuilt, this
// narrowness is the ONLY thing keeping a recorded case variant out of the
// executable drain — widen it and variants become executable.
const rows = this.db
.prepare(
`SELECT * FROM outbox_entries
@@ -688,10 +688,10 @@ export class QueueRepository {
if (!this.outbox.claimForDelivery(outboxId)) return "skipped";
const intent = this.outbox.getById(outboxId);
if (!intent) return "skipped"; // unreachable post-claim; defensive
// MF5 (defense in depth): only deliver a wake for a qitem that actually exists.
// A wake intent whose target qitem is missing — a forgery that slipped the
// route reservation, or a successor already swept — is finalized `failed`,
// never sent as a real wake.
// Only deliver a wake for a qitem that actually exists. A wake intent whose
// target qitem is missing — a caller-recorded id under this prefix (the route
// no longer refuses those), or a successor already swept — is finalized
// `failed`, never sent as a real wake.
if (!intent.auditPointer || !this.getById(intent.auditPointer)) {
this.outbox.finalizeDelivery(outboxId, "failed");
return "failed";
@@ -370,11 +370,6 @@ export interface SendOpts {
verify?: boolean;
force?: boolean;
waitForIdleMs?: number;
/** Final fail-loud check after any idle wait and immediately before delivery. */
beforeSend?: () =>
| { reason: string; error?: string }
| null
| Promise<{ reason: string; error?: string } | null>;
// OPR.0.4.1.10 — interactive-prompt / permission guard.
// `dangerouslyInteract` is the ONLY override of the prompt/permission guard (force does NOT bypass
// it). It requires `reason` and writes an auditable `transport.prompt_override` record before the
@@ -951,31 +946,6 @@ export class SessionTransport {
text = appendDeliveredSegment(text, this.now().getTime() - Date.parse(opts.stampISO));
}
if (opts?.beforeSend) {
try {
const refusal = await opts.beforeSend();
if (refusal) {
return {
ok: false,
sessionName,
reason: refusal.reason,
error: refusal.error,
sent: false,
...waitEvidence,
};
}
} catch (error) {
return {
ok: false,
sessionName,
reason: "before_send_check_failed",
error: error instanceof Error ? error.message : String(error),
sent: false,
...waitEvidence,
};
}
}
// 3. Send text (paste)
const textResult = await this.runStage(
"session_transport.send_text",
@@ -116,7 +116,6 @@ export const SETTINGS_VALID_KEYS = [
"policies.claude_compaction.message_inline",
"policies.claude_compaction.message_file_path",
"policies.claude_compaction.post_restore_audit_instruction",
"policies.claude_compaction.authorize_ttl_minutes",
"policies.idle_gate_qitem.scan_interval_seconds",
"policies.idle_gate_qitem.active_wake_interval_seconds",
"snapshots.periodic.enabled",
@@ -193,7 +192,6 @@ const ENV_MAP: Record<SettingsValidKey, { primary: string; legacy?: string }> =
"policies.claude_compaction.message_inline": { primary: "OPENRIG_POLICIES_CLAUDE_COMPACTION_MESSAGE_INLINE" },
"policies.claude_compaction.message_file_path": { primary: "OPENRIG_POLICIES_CLAUDE_COMPACTION_MESSAGE_FILE_PATH" },
"policies.claude_compaction.post_restore_audit_instruction": { primary: "OPENRIG_POLICIES_CLAUDE_COMPACTION_POST_RESTORE_AUDIT_INSTRUCTION" },
"policies.claude_compaction.authorize_ttl_minutes": { primary: "OPENRIG_POLICIES_CLAUDE_COMPACTION_AUTHORIZE_TTL_MINUTES" },
"policies.idle_gate_qitem.scan_interval_seconds": { primary: "OPENRIG_POLICIES_IDLE_GATE_QITEM_SCAN_INTERVAL_SECONDS" },
"policies.idle_gate_qitem.active_wake_interval_seconds": { primary: "OPENRIG_POLICIES_IDLE_GATE_QITEM_ACTIVE_WAKE_INTERVAL_SECONDS" },
"snapshots.periodic.enabled": { primary: "OPENRIG_SNAPSHOTS_PERIODIC_ENABLED" },
@@ -251,7 +249,6 @@ const KEY_TO_PATH: Record<SettingsValidKey, string[]> = {
"policies.claude_compaction.message_inline": ["policies", "claudeCompaction", "messageInline"],
"policies.claude_compaction.message_file_path": ["policies", "claudeCompaction", "messageFilePath"],
"policies.claude_compaction.post_restore_audit_instruction": ["policies", "claudeCompaction", "postRestoreAuditInstruction"],
"policies.claude_compaction.authorize_ttl_minutes": ["policies", "claudeCompaction", "authorizeTtlMinutes"],
"policies.idle_gate_qitem.scan_interval_seconds": ["policies", "idleGateQitem", "scanIntervalSeconds"],
"policies.idle_gate_qitem.active_wake_interval_seconds": ["policies", "idleGateQitem", "activeWakeIntervalSeconds"],
"snapshots.periodic.enabled": ["snapshots", "periodic", "enabled"],
@@ -480,7 +477,6 @@ function getDefaultValue(key: SettingsValidKey, workspaceRoot: string): string |
case "policies.claude_compaction.message_inline": return DEFAULT_CLAUDE_COMPACTION_RESTORE_INSTRUCTION;
case "policies.claude_compaction.message_file_path": return defaultClaudeCompactionExtraInstructionFilePath();
case "policies.claude_compaction.post_restore_audit_instruction": return DEFAULT_CLAUDE_COMPACTION_POST_RESTORE_AUDIT_INSTRUCTION;
case "policies.claude_compaction.authorize_ttl_minutes": return 15;
case "policies.idle_gate_qitem.scan_interval_seconds": return 60;
case "policies.idle_gate_qitem.active_wake_interval_seconds": return 900;
case "snapshots.periodic.enabled": return true;
@@ -556,19 +552,6 @@ const KEY_CONSTRAINTS: Partial<Record<SettingsValidKey, (raw: string, coerced: s
);
}
},
"policies.claude_compaction.authorize_ttl_minutes": (raw, coerced) => {
const trimmed = (raw ?? "").trim();
if (!/^-?\d+$/.test(trimmed) || typeof coerced !== "number" || !Number.isInteger(coerced)) {
throw new Error(
`Invalid value for policies.claude_compaction.authorize_ttl_minutes: expected an integer in [1, 60], got "${raw}"`,
);
}
if (coerced < 1 || coerced > 60) {
throw new Error(
`Invalid value for policies.claude_compaction.authorize_ttl_minutes: must be in [1, 60], got ${coerced}`,
);
}
},
"snapshots.periodic.interval_seconds": (raw, coerced) => {
if (!/^-?\d+$/.test((raw ?? "").trim())) {
throw new Error(
@@ -660,7 +643,6 @@ export interface ClaudeCompactionPolicy {
messageInline: string;
messageFilePath: string;
postRestoreAuditInstruction: string;
authorizeTtlMinutes: number;
}
export interface ResolvedConfig {
@@ -781,7 +763,6 @@ export class SettingsStore {
messageInline: this.resolveOne("policies.claude_compaction.message_inline", fc, wr).value as string,
messageFilePath: this.resolveOne("policies.claude_compaction.message_file_path", fc, wr).value as string,
postRestoreAuditInstruction: this.resolveOne("policies.claude_compaction.post_restore_audit_instruction", fc, wr).value as string,
authorizeTtlMinutes: this.resolveOne("policies.claude_compaction.authorize_ttl_minutes", fc, wr).value as number,
};
}
+1 -149
View File
@@ -3,14 +3,7 @@ import type Database from "better-sqlite3";
import type { ClaudeCompactionEnforcer } from "../domain/claude-compaction-enforcer.js";
import type { ContextUsageStore } from "../domain/context-usage-store.js";
import type { SessionTransport } from "../domain/session-transport.js";
import type { SessionRegistry } from "../domain/session-registry.js";
import {
AUTHORIZABLE_COMPACTION_REASONS,
CLAUDE_COMPACTION_ENFORCER_KIND,
type EnforcerDecisionStore,
} from "../domain/enforcer-decision-store.js";
import { authBearerTokenMiddleware } from "../middleware/auth-bearer-token.js";
import { requireSenderIdentity } from "./require-sender-identity.js";
/**
* OPR.0.4.3.14 — manual configurable compaction trigger route.
@@ -28,107 +21,6 @@ export function compactionRoutes(opts?: { bearerToken?: string | null }): Hono {
const router = new Hono();
router.use("*", authBearerTokenMiddleware({ expectedToken: opts?.bearerToken ?? null }));
router.post("/control", async (c) => {
const identity = requireSenderIdentity(c, { verb: "compaction control create" });
if (!identity.ok) return identity.response;
const store = c.get("enforcerDecisionStore" as never) as EnforcerDecisionStore | undefined;
const sessionRegistry = c.get("sessionRegistry" as never) as SessionRegistry | undefined;
if (!store || !sessionRegistry) {
return c.json({ ok: false, error: "compaction_control_unavailable" }, 503);
}
type ControlBody = {
session?: string;
direction?: string;
automaticReason?: string;
reason?: string;
};
const body = await c.req.json<ControlBody>().catch((): ControlBody => ({}));
const sessionName = body.session?.trim();
if (!sessionName) return c.json({ ok: false, error: "session_required" }, 400);
const reason = body.reason?.trim();
if (!reason) return c.json({ ok: false, error: "reason_required" }, 400);
if (body.direction !== "hold" && body.direction !== "authorize") {
return c.json({ ok: false, error: "direction_invalid" }, 400);
}
let automaticReason: string | null = null;
if (body.direction === "authorize") {
automaticReason = body.automaticReason?.trim() ?? "";
if (!automaticReason) {
return c.json({ ok: false, error: "automatic_reason_required" }, 400);
}
if (!(AUTHORIZABLE_COMPACTION_REASONS as readonly string[]).includes(automaticReason)) {
return c.json({ ok: false, error: "automatic_reason_not_authorizable" }, 400);
}
}
const currentSession = currentSessionName(sessionRegistry.db, sessionName);
if (currentSession && currentSession !== sessionName) {
return c.json({
ok: false,
error: "session_not_current",
currentSession,
}, 409);
}
const generationUuid = sessionRegistry.currentOccupantGenerationForSession(sessionName);
if (!generationUuid) {
return c.json({ ok: false, error: "generation_unavailable" }, 409);
}
try {
const decision = store.create({
enforcerKind: CLAUDE_COMPACTION_ENFORCER_KIND,
sessionName,
generationUuid,
direction: body.direction,
automaticReason,
reason,
actorSession: identity.session,
identityProvenance: "transport:v1",
});
return c.json({ ok: true, decision }, 201);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const status = /active hold|already exists/i.test(message) ? 409 : 400;
return c.json({ ok: false, error: "decision_rejected", message }, status as 400 | 409);
}
});
router.get("/control", (c) => {
const store = c.get("enforcerDecisionStore" as never) as EnforcerDecisionStore | undefined;
if (!store) return c.json({ ok: false, error: "compaction_control_unavailable" }, 503);
const sessionName = c.req.query("session")?.trim();
return c.json({
ok: true,
decisions: store.list(sessionName ? { sessionName } : {}),
});
});
router.post("/control/:decisionId/clear", async (c) => {
const identity = requireSenderIdentity(c, { verb: "compaction control clear" });
if (!identity.ok) return identity.response;
const store = c.get("enforcerDecisionStore" as never) as EnforcerDecisionStore | undefined;
if (!store) return c.json({ ok: false, error: "compaction_control_unavailable" }, 503);
type ClearBody = { reason?: string };
const body = await c.req.json<ClearBody>().catch((): ClearBody => ({}));
const reason = body.reason?.trim();
if (!reason) return c.json({ ok: false, error: "reason_required" }, 400);
try {
const decision = store.clear({
decisionId: c.req.param("decisionId"),
actorSession: identity.session,
identityProvenance: "transport:v1",
reason,
});
return c.json({ ok: true, decision });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return c.json({ ok: false, error: "decision_not_active", message }, 404);
}
});
router.post("/trigger", async (c) => {
const enforcer = c.get("compactionEnforcer" as never) as ClaudeCompactionEnforcer | undefined;
const transport = c.get("sessionTransport" as never) as SessionTransport | undefined;
@@ -156,15 +48,6 @@ export function compactionRoutes(opts?: { bearerToken?: string | null }): Hono {
return c.json({ ok: false, error: resolved.error }, status);
}
const currentSession = currentSessionName(db, sessionName);
if (currentSession && currentSession !== sessionName) {
return c.json({
ok: false,
error: "session_not_current",
currentSession,
}, 409);
}
// Resolve the DB node id + runtime for the latest session row.
const row = db.prepare(`
SELECT n.id AS node_id, n.runtime AS runtime
@@ -197,10 +80,7 @@ export function compactionRoutes(opts?: { bearerToken?: string | null }): Hono {
// This is the OPERATOR's manual-trigger verb (bearer-auth'd); the resulting sequence is
// drain-exempt while auto-compaction is disabled. GHOST-STAGE fix (a) actor-gate: automation
// paths that do NOT set this are NOT exempt, so they cannot launder a drain past the gate.
{
operatorInitiated: true,
resolveCurrentSessionName: (candidate) => currentSessionName(db, candidate),
},
{ operatorInitiated: true },
);
if (outcome.triggered) {
@@ -220,9 +100,6 @@ export function compactionRoutes(opts?: { bearerToken?: string | null }): Hono {
tmux_unavailable: 503,
send_failed: 502,
submit_failed: 502,
human_hold: 409,
session_not_current: 409,
occupant_generation_changed: 409,
};
const status = (statusMap[outcome.reason] ?? 409) as 400 | 404 | 409 | 422 | 502 | 503;
return c.json({
@@ -230,7 +107,6 @@ export function compactionRoutes(opts?: { bearerToken?: string | null }): Hono {
session: sessionName,
stage: outcome.stage,
reason: outcome.reason,
...(outcome.decisionId ? { decisionId: outcome.decisionId } : {}),
error: manualReasonMessage(sessionName, outcome.reason),
}, status);
});
@@ -250,24 +126,6 @@ export function compactionRoutes(opts?: { bearerToken?: string | null }): Hono {
return router;
}
function currentSessionName(db: Database.Database, sessionName: string): string | null {
const row = db.prepare(`
WITH target AS (
SELECT node_id
FROM sessions
WHERE session_name = ?
ORDER BY id DESC
LIMIT 1
)
SELECT session_name
FROM sessions
WHERE node_id = (SELECT node_id FROM target)
ORDER BY id DESC
LIMIT 1
`).get(sessionName) as { session_name: string } | undefined;
return row?.session_name ?? null;
}
function manualReasonMessage(sessionName: string, reason: string): string {
switch (reason) {
case "runtime_filter":
@@ -282,12 +140,6 @@ function manualReasonMessage(sessionName: string, reason: string): string {
return `Refused: '${sessionName}' activity could not be determined; failing closed so /compact cannot land on a prompt.`;
case "wait_for_idle_timeout":
return `Prep was sent to '${sessionName}' but it did not go idle in time, so /compact was NOT sent. Retry once the prep turn completes.`;
case "human_hold":
return `Refused: '${sessionName}' has an active human compaction hold.`;
case "session_not_current":
return `Refused: '${sessionName}' is no longer the current session for its node.`;
case "occupant_generation_changed":
return `Refused: '${sessionName}' changed occupant generation during compaction preparation.`;
default:
return `Manual compaction for '${sessionName}' did not complete (${reason}).`;
}
+1 -13
View File
@@ -13,7 +13,7 @@ import { requireSenderIdentity } from "./require-sender-identity.js";
import { hostname as osHostname } from "node:os";
import type { InboxHandler } from "../domain/inbox-handler.js";
import { InboxHandlerError } from "../domain/inbox-handler.js";
import { WAKE_INTENT_PREFIX, type OutboxHandler } from "../domain/outbox-handler.js";
import { type OutboxHandler } from "../domain/outbox-handler.js";
import { aggregateAttention } from "../domain/feed/attention-aggregator.js";
import type { AttentionItem } from "../domain/feed/attention-aggregator.js";
import { loadHostRegistry, resolveHost } from "../domain/hosts/hosts-registry-reader.js";
@@ -1007,18 +1007,6 @@ export function queueRoutes(): Hono {
// P21 I3: the outbox sender is the transport-derived identity, never body.senderSession.
const identity = requireSenderIdentity(c, { verb: "outbox record", bodyClaim: body.senderSession });
if (!identity.ok) return identity.response;
// MF5: the wake-intent id namespace is RESERVED for daemon-internal wake intents
// that the startup drain EXECUTES. A public audit write must not be able to
// forge one (an ordinary /outbox/record would otherwise become a real startup
// wake for an arbitrary/nonexistent qitem). BLOCKING 2 (guard re-seal): reject
// CASE VARIANTS too (toLowerCase) so the reservation is at least as broad as any
// selector — a `WAKE-INTENT-…` variant must never slip the route.
if (body.outboxId && body.outboxId.toLowerCase().startsWith(WAKE_INTENT_PREFIX)) {
return c.json(
{ error: `outboxId prefix '${WAKE_INTENT_PREFIX}' (any case) is reserved for daemon-internal wake intents and may not be set by callers` },
400,
);
}
if (!body.destinationSession) return c.json({ error: "destinationSession is required" }, 400);
if (!body.body) return c.json({ error: "body is required" }, 400);
-2
View File
@@ -297,7 +297,6 @@ export interface AppDeps {
* auto share one back-half state machine — no second restore path).
*/
compactionEnforcer?: ClaudeCompactionEnforcer;
enforcerDecisionStore?: import("./domain/enforcer-decision-store.js").EnforcerDecisionStore;
/**
* GHOST-STAGE (e/Class-B) — the canonical OccupantInvalidator, constructed once in startup and
* injected so SeatHandoverService.commit()'s re-key call actually FIRES (dev-driver's fold added
@@ -539,7 +538,6 @@ export function createApp(deps: AppDeps): Hono {
c.set("contextUsageStore" as never, deps.contextUsageStore);
c.set("contextMonitor" as never, deps.contextMonitor);
c.set("compactionEnforcer" as never, deps.compactionEnforcer);
c.set("enforcerDecisionStore" as never, deps.enforcerDecisionStore);
c.set("occupantInvalidator" as never, deps.occupantInvalidator);
c.set("nodeCmuxService" as never, deps.nodeCmuxService);
c.set("agentActivityStore" as never, deps.agentActivityStore);
+2 -12
View File
@@ -1763,24 +1763,14 @@ export async function createDaemon(opts?: DaemonOptions): Promise<DaemonResult>
// the type narrow for downstream consumers).
const { ContextMonitor } = await import("./domain/context-monitor.js");
const { ClaudeCompactionEnforcer } = await import("./domain/claude-compaction-enforcer.js");
const { EnforcerDecisionStore } = await import("./domain/enforcer-decision-store.js");
const compactionSettingsStore = new ContextPackSettingsStore();
const enforcerDecisionStore = new EnforcerDecisionStore(db, {
now: () => new Date(),
authorizeTtlMinutes: () => compactionSettingsStore.resolveClaudeCompactionPolicy().authorizeTtlMinutes,
});
deps.enforcerDecisionStore = enforcerDecisionStore;
const compactionEnforcer = deps.sessionTransport
? new ClaudeCompactionEnforcer(
compactionSettingsStore,
new ContextPackSettingsStore(),
deps.sessionTransport,
// GHOST-STAGE (b): resolve the LIVE occupant generation (atom-B tenure ledger) for a session
// so a stage minted by a retired generation is refused for the successor. null = UNKNOWN → the
// gate is inert (never compares a stale generation as if live).
{
resolveOccupantGeneration: (sessionName) => sessionRegistry.currentOccupantGenerationForSession(sessionName),
decisionStore: enforcerDecisionStore,
},
{ resolveOccupantGeneration: (sessionName) => sessionRegistry.currentOccupantGenerationForSession(sessionName) },
)
: undefined;
// 51-08 A1 — the over-time series rides the SAME 30s tick (PM decision 1):
@@ -911,10 +911,7 @@ describe("ClaudeCompactionEnforcer", () => {
expect(send.mock.calls[0]![2]).toBeUndefined();
// Phase 2 — /compact WITH the trust-bridge AND wait-for-idle (two-phase).
expect(send.mock.calls[1]![1]).toContain("/compact In the continuity summary, preserve this trust-channel note");
expect(send.mock.calls[1]![2]).toEqual({
waitForIdleMs: expect.any(Number),
beforeSend: expect.any(Function),
});
expect(send.mock.calls[1]![2]).toEqual({ waitForIdleMs: expect.any(Number) });
expect(send).toHaveBeenCalledTimes(2);
// The SAME maybeAutoCompact back-half (single restore path) drains it.
@@ -990,73 +987,6 @@ describe("ClaudeCompactionEnforcer", () => {
await first;
});
it("a retiring invocation refusal does not clobber its same-name successor's preparing marker", async () => {
const settings = makeSettingsStore(POLICY_ENABLED_AT_80);
let reachRetireeBoundary!: () => void;
let releaseRetiree!: () => void;
let reachSuccessorBoundary!: () => void;
let releaseSuccessor!: () => void;
const retireeAtBoundary = new Promise<void>((resolve) => { reachRetireeBoundary = resolve; });
const retireeReleased = new Promise<void>((resolve) => { releaseRetiree = resolve; });
const successorAtBoundary = new Promise<void>((resolve) => { reachSuccessorBoundary = resolve; });
const successorReleased = new Promise<void>((resolve) => { releaseSuccessor = resolve; });
let compactAttempt = 0;
const send = vi.fn(async (
_sessionName: string,
message: string,
opts?: { beforeSend?: () => { reason: string; error?: string } | null },
) => {
if (!message.startsWith("/compact")) return { ok: true as const };
compactAttempt += 1;
if (compactAttempt === 1) {
reachRetireeBoundary();
await retireeReleased;
} else if (compactAttempt === 2) {
reachSuccessorBoundary();
await successorReleased;
} else {
return { ok: false as const, reason: "unexpected_duplicate" };
}
const refusal = opts?.beforeSend?.();
return refusal
? { ok: false as const, sent: false, sessionName: SEAT, ...refusal }
: { ok: true as const };
});
const enforcer = new ClaudeCompactionEnforcer(
settings,
{ send } as unknown as SessionTransport,
{ openrigHome: HOME },
);
const retiree = enforcer.triggerManualCompact({ sessionName: SEAT, runtime: "claude-code", usedPercentage: 20 });
await retireeAtBoundary;
enforcer.invalidateOccupant(SEAT);
const successor = enforcer.triggerManualCompact({ sessionName: SEAT, runtime: "claude-code", usedPercentage: 20 });
await successorAtBoundary;
expect(enforcer.getManualCompactionState(SEAT)?.stage).toBe("preparing");
releaseRetiree();
const retireeOutcome = await retiree;
const stateAfterRetiree = enforcer.getManualCompactionState(SEAT)?.stage;
const sendsBeforeThird = send.mock.calls.length;
const third = await enforcer.triggerManualCompact({ sessionName: SEAT, runtime: "claude-code", usedPercentage: 20 });
const sendsAfterThird = send.mock.calls.length;
releaseSuccessor();
const successorOutcome = await successor;
expect(retireeOutcome).toEqual({
triggered: false,
stage: "skipped-or-failed",
reason: "occupant_generation_changed",
});
expect(stateAfterRetiree).toBe("preparing");
expect(third).toEqual({ triggered: false, stage: "skipped-or-failed", reason: "already_in_progress" });
expect(sendsAfterThird).toBe(sendsBeforeThird);
expect(successorOutcome).toEqual({ triggered: true, stage: "compact-sent" });
});
it("sequential call after compact-sent but before the back-half drains: no second prep, no second /compact", async () => {
const settings = makeSettingsStore(POLICY_ENABLED_AT_80);
const { transport, send } = makeSessionTransport();
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type Database from "better-sqlite3";
import { createFullTestDb } from "./helpers/test-app.js";
import { EventBus } from "../src/domain/event-bus.js";
@@ -14,10 +14,7 @@ describe("W2b exact-set notify envelope", () => {
bus = new EventBus(db);
});
afterEach(() => {
vi.restoreAllMocks();
db.close();
});
afterEach(() => db.close());
function persisted(label: string) {
return bus.persistWithinTransaction({
@@ -192,65 +189,6 @@ describe("W2b exact-set notify envelope", () => {
});
});
it("retries a malformed row when its first poison write fails", () => {
const received: PersistedEvent[] = [];
bus.subscribe((event) => received.push(event));
const initialStatus = bus.getNotifyDrainStatus();
const persist = bus.persistWithinTransaction.bind(bus);
let rejectFirstPoison = true;
vi.spyOn(bus, "persistWithinTransaction").mockImplementation((event) => {
if (event.type === "event.delivery_poisoned" && rejectFirstPoison) {
rejectFirstPoison = false;
throw new Error("simulated poison write failure");
}
return persist(event);
});
let malformedSeq = 0;
let laterSeq = 0;
expect(() =>
bus.withNotifyEnvelope(() => {
malformedSeq = Number(
db.prepare("INSERT INTO events (rig_id, node_id, type, payload) VALUES (?, ?, ?, ?)")
.run(null, null, "malformed.fixture", "{not-json")
.lastInsertRowid,
);
laterSeq = Number(
db.prepare("INSERT INTO events (rig_id, node_id, type, payload) VALUES (?, ?, ?, ?)")
.run(
null,
null,
"view.changed",
JSON.stringify({ type: "view.changed", viewName: "later", cause: "poison-retry" }),
)
.lastInsertRowid,
);
}),
).toThrow("simulated poison write failure");
expect(bus.getNotifyDrainStatus()).toEqual(initialStatus);
expect(db.prepare("SELECT COUNT(*) AS count FROM events WHERE type = 'event.delivery_poisoned'").get())
.toEqual({ count: 0 });
expect(received).toEqual([]);
bus.withNotifyEnvelope(() => undefined);
const poisonRows = db.prepare(
"SELECT seq, payload FROM events WHERE type = 'event.delivery_poisoned' ORDER BY seq",
).all() as Array<{ seq: number; payload: string }>;
expect(poisonRows).toHaveLength(1);
expect(JSON.parse(poisonRows[0]!.payload)).toMatchObject({
type: "event.delivery_poisoned",
poisonedSeq: malformedSeq,
});
expect(received.map((event) => event.seq)).toEqual([laterSeq, poisonRows[0]!.seq]);
expect(received.map((event) => event.type)).toEqual(["view.changed", "event.delivery_poisoned"]);
expect(bus.getNotifyDrainStatus()).toMatchObject({
state: "unparseable",
lastPoison: { seq: malformedSeq },
});
});
it.each([
["null", "null"],
["array", "[]"],
+2 -1
View File
@@ -146,7 +146,8 @@ export const migrationsForFullTestDbExclusions: Record<string, string> = {
"062_usage_samples.sql": "usage-metering subsystem table — usage suites migrate it inline.",
"065_identity_provenance.sql": "P21 additive era-stamp column on 037_mission_control_actions (itself excluded) — mission-control / review-freeze / scope suites migrate it inline where they assert provenance.",
"067_i3_identity_provenance.sql": "P21 additive era-stamp columns; it ALTERs inbox_entries + outbox_entries (both excluded from the core edge) alongside queue_transitions/stream_items, so it cannot ride the core-edge list — queue / inbox / outbox / stream suites asserting provenance migrate it inline.",
"068_enforcer_decisions.sql": "compaction-enforcement decision subsystem table — W4 suites migrate ALL_MIGRATIONS inline; no shared full-test DB consumer reads it.",
"068_enforcer_decisions.sql": "W4 compaction-enforcement decision table — the suite was unbuilt and 071 drops the table forward; 068 stays as applied history. No consumer reads it, and 071 is IF EXISTS so it is a no-op on this fixture.",
"071_drop_enforcer_decisions.sql": "The forward drop of 068, excluded above — this fixture never creates enforcer_decisions, so applying its drop would be a no-op (the DROP is IF EXISTS). Re-check together with 068: if 068 is ever added to the core edge, add this one too or the fixture keeps a table the shipped schema has dropped.",
};
export function createFullTestDb(): Database.Database {
-29
View File
@@ -124,35 +124,6 @@ describe("queue routes", () => {
return ((await res.json()) as { inboxId: string }).inboxId;
}
// MF5 (SECURITY): the executable wake-intent id namespace is RESERVED — a public
// /outbox/record audit write must not be able to forge a startup wake.
it("outbox record — 400 REJECTS a caller-supplied wake-intent-* outboxId (reserved namespace)", async () => {
const res = await app.request("/api/queue/outbox/record", {
method: "POST",
headers: { "Content-Type": "application/json", "X-OpenRig-Session": "attacker@rig" },
body: JSON.stringify({
outboxId: "wake-intent-forged",
destinationSession: "victim@rig",
body: "forged wake payload",
}),
});
expect(res.status).toBe(400);
const err = (await res.json()) as { error: string };
expect(err.error).toMatch(/wake-intent-|reserved/i);
// and nothing was written under the forged id
expect(outbox.getById("wake-intent-forged")).toBeNull();
});
it("outbox record — 400 REJECTS a CASE-VARIANT wake-intent id (BLOCKING 2: route ⊇ selector)", async () => {
const res = await app.request("/api/queue/outbox/record", {
method: "POST",
headers: { "Content-Type": "application/json", "X-OpenRig-Session": "attacker@rig" },
body: JSON.stringify({ outboxId: "WAKE-INTENT-forged", destinationSession: "victim@rig", body: "variant" }),
});
expect(res.status).toBe(400);
expect(outbox.getById("WAKE-INTENT-forged")).toBeNull();
});
it("outbox record — still accepts a normal (non-reserved) outboxId", async () => {
const res = await app.request("/api/queue/outbox/record", {
method: "POST",
@@ -557,10 +557,11 @@ describe("W1 MF4 — the intent freezes its emitting generation/envelope", () =>
});
});
// MF5 (guard HOLD, SECURITY): the executable wake-intent namespace must not be
// forgeable. Route reservation is pinned in the queue-routes suite; here we pin
// the drain-side defense: a wake whose target qitem does not exist is never sent.
describe("W1 MF5 — the executable wake-intent namespace is not forgeable (drain side)", () => {
// The drain refuses a wake whose target qitem does not exist. The route-side prefix
// refusal was unbuilt (founder ruling), so a caller CAN record an id under the
// executable prefix; this pin is what stops such a row being sent as a real wake
// when it points at nothing.
describe("W1 — the drain never sends a wake for a nonexistent qitem", () => {
it("a wake-intent pointing at a NONEXISTENT qitem is failed, never sent", async () => {
const h = makeHarness();
h.outbox.record({
@@ -610,10 +611,13 @@ describe("W1 re-seal BLOCKING 1 — abandoned `sending` claims reconcile to inde
});
});
// Re-seal BLOCKING 2 (guard): the executable drain selector is EXACT-CASE, so a
// `WAKE-INTENT-…` variant (which the public route also now rejects) is never
// selected/executed.
describe("W1 re-seal BLOCKING 2 — the executable drain selector is exact-case", () => {
// The executable drain selector is EXACT-CASE, so a `WAKE-INTENT-…` variant is never
// selected/executed. NOTE: the public /outbox/record route no longer rejects reserved-prefix
// ids (the W4-era MF5 route guard was unbuilt — founder ruling: its justification required an
// adversary inside this trust domain, where the only caller is the daemon's own localhost
// client). That makes this selector pin MORE load-bearing, not less: it is now the only thing
// keeping a recorded case-variant id out of the executable drain.
describe("W1 — the executable drain selector is exact-case", () => {
it("a WAKE-INTENT- case variant is NOT selected by the drain (never executed)", async () => {
const h = makeHarness();
const q = await h.repo.create({ sourceSession: "driver@rig", destinationSession: "reviewer@rig", body: "z" });
@@ -495,47 +495,6 @@ describe("SessionTransport", () => {
expect(callOrder).toEqual(["capture", "capture", "sendText", "sendKeys"]);
});
it("send with wait-for-idle runs its final check after idle and can refuse before delivery", async () => {
seedCanonicalRig();
const callOrder: string[] = [];
let captureCount = 0;
const sendTextSpy = vi.fn(async () => {
callOrder.push("sendText");
return { ok: true as const };
});
const tmux = mockTmux({
capturePaneContent: async () => {
callOrder.push("capture");
captureCount++;
return captureCount === 1
? "Working on task...\n⠋ Processing files\nesc to interrupt"
: "Ready\n❯ ";
},
sendText: sendTextSpy,
});
const transport = createTransport(tmux, {
sleep: async () => undefined,
waitForIdlePollMs: 1,
});
const result = await transport.send("dev-impl@my-rig", "hello", {
waitForIdleMs: 50,
beforeSend: async () => {
callOrder.push("beforeSend");
return { reason: "human_hold", error: "Compaction hold became active." };
},
});
expect(result).toMatchObject({
ok: false,
reason: "human_hold",
sent: false,
attempts: 2,
});
expect(callOrder).toEqual(["capture", "capture", "beforeSend"]);
expect(sendTextSpy).not.toHaveBeenCalled();
});
it("send with wait-for-idle waits through current Claude thinking evidence and sends after idle", async () => {
seedCanonicalRig();
const callOrder: string[] = [];
@@ -28,7 +28,6 @@ function clearEnv(): () => void {
"OPENRIG_POLICIES_CLAUDE_COMPACTION_MESSAGE_INLINE",
"OPENRIG_POLICIES_CLAUDE_COMPACTION_MESSAGE_FILE_PATH",
"OPENRIG_POLICIES_CLAUDE_COMPACTION_POST_RESTORE_AUDIT_INSTRUCTION",
"OPENRIG_POLICIES_CLAUDE_COMPACTION_AUTHORIZE_TTL_MINUTES",
];
const saved: Record<string, string | undefined> = {};
for (const k of keys) {
@@ -79,7 +78,7 @@ describe("config routes (User Settings v0)", () => {
// + 2 V1 pre-release Item 1 (transcripts.lines / transcripts.poll_interval_seconds)
// + 1 plugin-primitive Phase 3a slice 3.5 (runtime.codex.hooks_enabled)
// + 1 V0.3.1 slice 05 (workspace.operator_seat_name)
// + 8 slice 27 + W4 (policies.claude_compaction.*)
// + 7 slice 27 (policies.claude_compaction.*)
// + 3 OPR.0.3.4.9 (snapshots.periodic.*)
// + 1 OPR.0.4.0.1 (ui.terminal.max_live_terminals)
// + 2 OPR.0.4.6.MH1 (host.selected / host.name)
@@ -89,18 +88,14 @@ describe("config routes (User Settings v0)", () => {
// watchdog_keep_per_job / batch_size — the CLI-settable queue-retention knobs;
// + retention.usage_samples_days, 51-08 A2)
// + 2 OPR.0.5.1 W2c (policies.idle_gate_qitem.scan_interval_seconds /
// active_wake_interval_seconds) → 53 total.
expect(Object.keys(body.settings).length).toBe(53);
// active_wake_interval_seconds) → 52 total.
expect(Object.keys(body.settings).length).toBe(52);
expect(body.settings["daemon.port"]?.source).toBe("default");
expect(body.settings["ui.preview.refresh_interval_seconds"]?.value).toBe(3);
expect(body.settings["ui.preview.max_pins"]?.value).toBe(4);
expect(body.settings["ui.preview.default_lines"]?.value).toBe(50);
expect(body.settings["recovery.auto_drive_provider_prompts"]?.value).toBe(false);
expect(body.settings["recovery.provider_auth_env_allowlist"]?.value).toBe("");
expect(body.settings["policies.claude_compaction.authorize_ttl_minutes"]).toMatchObject({
value: 15,
source: "default",
});
expect(body.settings["host.selected"]).toMatchObject({ value: "local", source: "default" });
expect(body.settings["host.name"]).toMatchObject({ value: "localhost", source: "default" });
expect(String(body.settings["workspace.dogfood_evidence_root"]?.value)).toMatch(/dogfood-evidence$/);
@@ -49,7 +49,6 @@ function clearEnv(): () => void {
"OPENRIG_POLICIES_CLAUDE_COMPACTION_MESSAGE_INLINE",
"OPENRIG_POLICIES_CLAUDE_COMPACTION_MESSAGE_FILE_PATH",
"OPENRIG_POLICIES_CLAUDE_COMPACTION_POST_RESTORE_AUDIT_INSTRUCTION",
"OPENRIG_POLICIES_CLAUDE_COMPACTION_AUTHORIZE_TTL_MINUTES",
"OPENRIG_POLICIES_IDLE_GATE_QITEM_SCAN_INTERVAL_SECONDS",
"OPENRIG_POLICIES_IDLE_GATE_QITEM_ACTIVE_WAKE_INTERVAL_SECONDS",
"RIGGED_PORT", "RIGGED_HOST", "RIGGED_DB",
@@ -126,7 +125,6 @@ describe("SettingsStore (User Settings v0)", () => {
"policies.claude_compaction.message_inline",
"policies.claude_compaction.message_file_path",
"policies.claude_compaction.post_restore_audit_instruction",
"policies.claude_compaction.authorize_ttl_minutes",
// OPR.0.5.1 51-06 W2c — tunable daemon auto-registration cadence.
"policies.idle_gate_qitem.scan_interval_seconds",
"policies.idle_gate_qitem.active_wake_interval_seconds",
@@ -341,7 +339,6 @@ describe("SettingsStore (User Settings v0)", () => {
expect(policy.messageFilePath).toMatch(/^\//);
expect(policy.messageFilePath.endsWith(DEFAULT_EXTRA_INSTRUCTION_FILE_SUFFIX)).toBe(true);
expect(policy.postRestoreAuditInstruction).toContain(DEFAULT_AUDIT_INSTRUCTION_FRAGMENT);
expect(policy.authorizeTtlMinutes).toBe(15);
});
it("HG-10: resolveClaudeCompactionPolicy picks up direct config.json edits without daemon restart (single resolve call rereads file)", () => {
@@ -360,7 +357,6 @@ describe("SettingsStore (User Settings v0)", () => {
messageInline: "carry-forward note",
messageFilePath: "",
postRestoreAuditInstruction: "verify the reads",
authorizeTtlMinutes: 30,
},
},
}),
@@ -374,7 +370,6 @@ describe("SettingsStore (User Settings v0)", () => {
expect(updated.messageInline).toBe("carry-forward note");
expect(updated.messageFilePath.endsWith(DEFAULT_EXTRA_INSTRUCTION_FILE_SUFFIX)).toBe(true);
expect(updated.postRestoreAuditInstruction).toBe("verify the reads");
expect(updated.authorizeTtlMinutes).toBe(30);
});
it("Slice 27: ensureDefaultClaudeCompactionFiles creates the user-owned extra instruction placeholder without overwriting edits", () => {
@@ -401,7 +396,6 @@ describe("SettingsStore (User Settings v0)", () => {
store.set("policies.claude_compaction.message_inline", "rehydrate the agent");
store.set("policies.claude_compaction.message_file_path", "/tmp/msg.txt");
store.set("policies.claude_compaction.post_restore_audit_instruction", "audit reads");
store.set("policies.claude_compaction.authorize_ttl_minutes", "30");
const raw = JSON.parse(require("node:fs").readFileSync(configPath, "utf-8"));
expect(raw.policies.claudeCompaction.enabled).toBe(true);
@@ -411,7 +405,6 @@ describe("SettingsStore (User Settings v0)", () => {
expect(raw.policies.claudeCompaction.messageInline).toBe("rehydrate the agent");
expect(raw.policies.claudeCompaction.messageFilePath).toBe("/tmp/msg.txt");
expect(raw.policies.claudeCompaction.postRestoreAuditInstruction).toBe("audit reads");
expect(raw.policies.claudeCompaction.authorizeTtlMinutes).toBe(30);
expect(store.resolveOne("policies.claude_compaction.enabled").value).toBe(true);
expect(store.resolveOne("policies.claude_compaction.threshold_percent").value).toBe(60);
@@ -420,7 +413,6 @@ describe("SettingsStore (User Settings v0)", () => {
expect(store.resolveOne("policies.claude_compaction.message_inline").value).toBe("rehydrate the agent");
expect(store.resolveOne("policies.claude_compaction.message_file_path").value).toBe("/tmp/msg.txt");
expect(store.resolveOne("policies.claude_compaction.post_restore_audit_instruction").value).toBe("audit reads");
expect(store.resolveOne("policies.claude_compaction.authorize_ttl_minutes").value).toBe(30);
});
it("HG-1: invalid threshold rejected by coerceValue (non-numeric raises)", () => {
@@ -428,70 +420,6 @@ describe("SettingsStore (User Settings v0)", () => {
expect(() => store.set("policies.claude_compaction.threshold_percent", "not-a-number")).toThrow(/expected a number/);
});
describe("W4 authorize TTL config contract", () => {
it("accepts the exact inclusive 1..60 range through the write path", () => {
const store = new SettingsStore(configPath);
for (const value of ["1", "15", "60"]) {
store.set("policies.claude_compaction.authorize_ttl_minutes", value);
expect(store.resolveOne("policies.claude_compaction.authorize_ttl_minutes")).toMatchObject({
value: Number(value),
source: "file",
defaultValue: 15,
});
}
for (const value of ["0", "61", "15.5", "bad"]) {
expect(() => store.set("policies.claude_compaction.authorize_ttl_minutes", value)).toThrow(/integer|\[1, 60\]|number/i);
}
});
it("uses a valid env override", () => {
process.env["OPENRIG_POLICIES_CLAUDE_COMPACTION_AUTHORIZE_TTL_MINUTES"] = "30";
expect(new SettingsStore(configPath).resolveOne(
"policies.claude_compaction.authorize_ttl_minutes",
)).toMatchObject({ value: 30, source: "env", defaultValue: 15 });
});
it.each(["0", "61", "15.5", "bad"])(
"invalid env %s falls back to 15 and reports loudly",
(raw) => {
process.env["OPENRIG_POLICIES_CLAUDE_COMPACTION_AUTHORIZE_TTL_MINUTES"] = raw;
const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
try {
const resolved = new SettingsStore(configPath).resolveOne(
"policies.claude_compaction.authorize_ttl_minutes",
);
expect(resolved).toMatchObject({ value: 15, source: "default", defaultValue: 15 });
expect(stderr.mock.calls.some((call) => String(call[0]).includes(
"env override for policies.claude_compaction.authorize_ttl_minutes rejected",
))).toBe(true);
} finally {
stderr.mockRestore();
}
},
);
it.each([0, 61, 15.5, "bad"])(
"invalid file value %s falls back to 15 and reports loudly",
(raw) => {
writeFileSync(configPath, JSON.stringify({
policies: { claudeCompaction: { authorizeTtlMinutes: raw } },
}));
const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
try {
const resolved = new SettingsStore(configPath).resolveOne(
"policies.claude_compaction.authorize_ttl_minutes",
);
expect(resolved).toMatchObject({ value: 15, source: "default", defaultValue: 15 });
expect(stderr.mock.calls.some((call) => String(call[0]).includes(
"file value for policies.claude_compaction.authorize_ttl_minutes rejected",
))).toBe(true);
} finally {
stderr.mockRestore();
}
},
);
});
// Slice 27 BLOCKING-FIX-2 — env + file source resolution must also
// reject invalid threshold values. Without this, an env override
// (OPENRIG_POLICIES_CLAUDE_COMPACTION_THRESHOLD_PERCENT=80abc) or a
@@ -1,136 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type Database from "better-sqlite3";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { createDb } from "../src/db/connection.js";
import { migrate } from "../src/db/migrate.js";
import { ALL_MIGRATIONS } from "../src/db/all-migrations.js";
import { RigRepository } from "../src/domain/rig-repository.js";
import { SessionRegistry } from "../src/domain/session-registry.js";
import { ContextUsageStore } from "../src/domain/context-usage-store.js";
import { ContextMonitor } from "../src/domain/context-monitor.js";
import { ClaudeCompactionEnforcer } from "../src/domain/claude-compaction-enforcer.js";
import type { SessionTransport } from "../src/domain/session-transport.js";
const STORE_MODULE = ["../src/domain", "enforcer-decision-store.js"].join("/");
const SESSION = "claude-seat@rig";
type DecisionStore = {
create(input: Record<string, unknown>): Record<string, unknown>;
list(input?: Record<string, unknown>): Array<Record<string, unknown>>;
};
type DecisionStoreConstructor = new (
db: Database.Database,
opts: { now: () => Date; authorizeTtlMinutes: () => number },
) => DecisionStore;
async function loadDecisionStore(): Promise<DecisionStoreConstructor> {
let mod: { EnforcerDecisionStore?: DecisionStoreConstructor } | null = null;
try {
mod = await import(/* @vite-ignore */ STORE_MODULE);
} catch {
// Assertion below keeps the RED feature-shaped instead of surfacing a loader error.
}
expect(mod, "durable enforcer decision store module must exist").not.toBeNull();
expect(mod?.EnforcerDecisionStore).toBeTypeOf("function");
return mod!.EnforcerDecisionStore!;
}
describe("W4 ContextMonitor enforcement observation", () => {
let db: Database.Database;
let stateDir: string;
let contextStore: ContextUsageStore;
let monitor: ContextMonitor | undefined;
let generationUuid: string;
beforeEach(() => {
db = createDb();
migrate(db, ALL_MIGRATIONS);
stateDir = join(tmpdir(), `w4-context-monitor-${process.pid}-${Math.random().toString(36).slice(2)}`);
mkdirSync(join(stateDir, "context"), { recursive: true });
contextStore = new ContextUsageStore(db, { stateDir, codexHomeDir: stateDir });
const rigs = new RigRepository(db);
const sessions = new SessionRegistry(db);
const rig = rigs.createRig("rig");
const node = rigs.addNode(rig.id, "claude.seat", { runtime: "claude-code" });
const session = sessions.registerSession(node.id, SESSION);
generationUuid = sessions.currentOccupantGenerationForSession(SESSION)!;
db.prepare("UPDATE sessions SET status = 'running' WHERE id = ?").run(session.id);
writeFileSync(
join(stateDir, "context", `${SESSION}.json`),
JSON.stringify({
session_name: SESSION,
sampled_at: "2026-08-08T18:00:30.000Z",
context_window: {
context_window_size: 200_000,
used_percentage: 90,
remaining_percentage: 10,
total_input_tokens: 170_000,
total_output_tokens: 10_000,
},
}),
);
});
afterEach(() => {
monitor?.stop();
db.close();
rmSync(stateDir, { recursive: true, force: true });
});
it("a real poll honors a hold with unknown live generation and exposes its observation", async () => {
const EnforcerDecisionStore = await loadDecisionStore();
const decisions = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:30.000Z"),
authorizeTtlMinutes: () => 15,
});
const hold = decisions.create({
enforcerKind: "claude_compaction",
sessionName: SESSION,
generationUuid,
direction: "hold",
reason: "finish the current atomic action",
actorSession: "orch-lead@rig",
identityProvenance: "transport:v1",
});
const send = vi.fn(async () => ({ ok: true as const }));
const enforcer = new ClaudeCompactionEnforcer(
{
resolveClaudeCompactionPolicy: () => ({
enabled: true,
thresholdPercent: 80,
preCompactInstruction: "prepare",
compactInstruction: "",
messageInline: "",
messageFilePath: "",
postRestoreAuditInstruction: "audit",
authorizeTtlMinutes: 15,
}),
} as never,
{ send } as unknown as SessionTransport,
{
resolveOccupantGeneration: () => null,
decisionStore: decisions,
} as never,
);
monitor = new ContextMonitor(
db,
contextStore,
{ ensureContextCollector: vi.fn() },
enforcer,
);
await monitor.pollOnce();
expect(send).not.toHaveBeenCalled();
expect(decisions.list({ sessionName: SESSION })).toContainEqual(expect.objectContaining({
decisionId: hold["decisionId"],
lastObservedAt: "2026-08-08T18:00:30.000Z",
lastObservedOutcome: "human_hold",
}));
});
});
@@ -1,589 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { Hono } from "hono";
import type Database from "better-sqlite3";
import { createDb } from "../src/db/connection.js";
import { migrate } from "../src/db/migrate.js";
import { ALL_MIGRATIONS } from "../src/db/all-migrations.js";
import { RigRepository } from "../src/domain/rig-repository.js";
import { SessionRegistry } from "../src/domain/session-registry.js";
import { ClaudeCompactionEnforcer } from "../src/domain/claude-compaction-enforcer.js";
import { EnforcerDecisionStore } from "../src/domain/enforcer-decision-store.js";
import { DefaultOccupantInvalidator } from "../src/domain/occupant-invalidator.js";
import { compactionRoutes } from "../src/routes/compaction.js";
const IDENTITY = "orch-lead@rig";
const SEAT = "claude-seat@rig";
describe("W4 compaction-control routes", () => {
let db: Database.Database;
let sessionRegistry: SessionRegistry;
let nodeId: string;
let generationUuid: string;
let decisions: {
create: ReturnType<typeof vi.fn>;
list: ReturnType<typeof vi.fn>;
clear: ReturnType<typeof vi.fn>;
};
let app: Hono;
let manualDeps: {
enforcer: ClaudeCompactionEnforcer;
transport: Record<string, unknown>;
usageStore: Record<string, unknown>;
} | null;
beforeEach(() => {
db = createDb();
migrate(db, ALL_MIGRATIONS);
const rigs = new RigRepository(db);
sessionRegistry = new SessionRegistry(db);
const rig = rigs.createRig("rig");
const node = rigs.addNode(rig.id, "claude.seat", { runtime: "claude-code" });
nodeId = node.id;
sessionRegistry.registerSession(node.id, SEAT);
generationUuid = sessionRegistry.currentOccupantGenerationForSession(SEAT)!;
decisions = {
create: vi.fn((input) => ({ decisionId: "decision-1", active: true, ...input })),
list: vi.fn(() => [{
decisionId: "hold-1",
sessionName: SEAT,
direction: "hold",
active: true,
lastObservedAt: "2026-08-08T18:00:30.000Z",
lastObservedOutcome: "human_hold",
}]),
clear: vi.fn((input) => ({ decisionId: input.decisionId, active: false, releaseKind: "cleared" })),
};
manualDeps = null;
app = new Hono();
app.use("*", async (c, next) => {
c.set("db" as never, db);
c.set("sessionRegistry" as never, sessionRegistry);
c.set("enforcerDecisionStore" as never, decisions);
if (manualDeps) {
c.set("compactionEnforcer" as never, manualDeps.enforcer);
c.set("sessionTransport" as never, manualDeps.transport);
c.set("contextUsageStore" as never, manualDeps.usageStore);
}
await next();
});
app.route("/api/compaction", compactionRoutes());
});
afterEach(() => db.close());
const request = (path: string, body?: Record<string, unknown>, identity = IDENTITY) =>
app.request(path, {
method: body === undefined ? "GET" : "POST",
headers: {
...(body === undefined ? {} : { "Content-Type": "application/json" }),
...(identity ? { "X-OpenRig-Session": identity } : {}),
},
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
function makeBoundaryTransport(afterPrep: () => void) {
const delivered: string[] = [];
const send = vi.fn(async (
_sessionName: string,
message: string,
opts?: {
beforeSend?: () =>
| { reason: string; error?: string }
| null
| Promise<{ reason: string; error?: string } | null>;
},
) => {
if (send.mock.calls.length === 1) {
delivered.push(message);
afterPrep();
return { ok: true as const };
}
const refusal = await opts?.beforeSend?.();
if (refusal) return { ok: false as const, sessionName: SEAT, sent: false, ...refusal };
delivered.push(message);
return { ok: true as const };
});
return {
delivered,
send,
transport: {
send,
resolveSessions: vi.fn(async () => ({ ok: true as const, sessions: [SEAT] })),
},
};
}
function installManualHarness(store: EnforcerDecisionStore, transport: Record<string, unknown>): void {
const enforcer = new ClaudeCompactionEnforcer(
{
resolveClaudeCompactionPolicy: () => ({
enabled: true,
thresholdPercent: 80,
preCompactInstruction: "prepare",
compactInstruction: "",
messageInline: "",
messageFilePath: "",
postRestoreAuditInstruction: "audit",
authorizeTtlMinutes: 15,
}),
} as never,
transport as never,
{
resolveOccupantGeneration: (sessionName: string) =>
sessionRegistry.currentOccupantGenerationForSession(sessionName),
decisionStore: store,
},
);
manualDeps = {
enforcer,
transport,
usageStore: {
getForNode: () => ({
availability: "known",
usedPercentage: 90,
transcriptPath: "/tmp/claude.jsonl",
sessionId: "claude-session",
}),
},
};
decisions = store as never;
}
it("refuses an unattributable hold before writing any record", async () => {
const response = await request("/api/compaction/control", {
session: SEAT,
direction: "hold",
reason: "mid-fold",
}, "");
expect(response.status).toBe(401);
expect(await response.json()).toMatchObject({ error: "unattributable_sender" });
expect(decisions.create).not.toHaveBeenCalled();
});
it("refuses blank reasons and blanket authorizations loudly", async () => {
const blank = await request("/api/compaction/control", {
session: SEAT,
direction: "hold",
reason: " ",
});
expect(blank.status).toBe(400);
expect(await blank.json()).toMatchObject({ error: "reason_required" });
const blanket = await request("/api/compaction/control", {
session: SEAT,
direction: "authorize",
reason: "ignore refusals",
});
expect(blanket.status).toBe(400);
expect(await blanket.json()).toMatchObject({ error: "automatic_reason_required" });
expect(decisions.create).not.toHaveBeenCalled();
});
it.each([
"runtime_filter",
"no_usage_data",
"invalid_policy",
"below_threshold",
"already_triggered_above_threshold",
"dedup_window",
"send_failed",
])("refuses excluded authorization reason %s", async (automaticReason) => {
const response = await request("/api/compaction/control", {
session: SEAT,
direction: "authorize",
automaticReason,
reason: "not in locked set",
});
expect(response.status).toBe(400);
expect(await response.json()).toMatchObject({ error: "automatic_reason_not_authorizable" });
expect(decisions.create).not.toHaveBeenCalled();
});
it("derives actor/provenance and generation when creating a hold", async () => {
const response = await request("/api/compaction/control", {
session: SEAT,
direction: "hold",
reason: "finish current atomic action",
});
expect(response.status).toBe(201);
expect(decisions.create).toHaveBeenCalledWith({
enforcerKind: "claude_compaction",
sessionName: SEAT,
generationUuid,
direction: "hold",
automaticReason: null,
reason: "finish current atomic action",
actorSession: IDENTITY,
identityProvenance: "transport:v1",
});
});
it.each(["disabled", "post_restore_cooldown", "stale_generation"])(
"creates one reason-matched authorization for %s without accepting caller expiry",
async (automaticReason) => {
const response = await request("/api/compaction/control", {
session: SEAT,
direction: "authorize",
automaticReason,
reason: `allow ${automaticReason} once`,
expiresAt: "2099-01-01T00:00:00.000Z",
});
expect(response.status).toBe(201);
expect(decisions.create).toHaveBeenCalledWith({
enforcerKind: "claude_compaction",
sessionName: SEAT,
generationUuid,
direction: "authorize",
automaticReason,
reason: `allow ${automaticReason} once`,
actorSession: IDENTITY,
identityProvenance: "transport:v1",
});
expect(decisions.create.mock.calls[0]?.[0]).not.toHaveProperty("expiresAt");
},
);
it("refuses decision creation when live generation cannot be resolved", async () => {
const response = await request("/api/compaction/control", {
session: "missing-seat@rig",
direction: "hold",
reason: "cannot scope without a generation",
});
expect(response.status).toBe(409);
expect(await response.json()).toMatchObject({ error: "generation_unavailable" });
expect(decisions.create).not.toHaveBeenCalled();
});
it.each(["hold", "authorize"] as const)(
"rejects historical session aliases for %s and accepts only the current alias",
async (direction) => {
const currentSeat = "claude-seat-next@rig";
sessionRegistry.registerSession(nodeId, currentSeat);
const automaticReason = direction === "authorize" ? "disabled" : undefined;
const body = {
session: SEAT,
direction,
...(automaticReason ? { automaticReason } : {}),
reason: `current-generation ${direction}`,
};
const stale = await request("/api/compaction/control", body);
expect(stale.status).toBe(409);
expect(await stale.json()).toMatchObject({
error: "session_not_current",
currentSession: currentSeat,
});
expect(decisions.create).not.toHaveBeenCalled();
const current = await request("/api/compaction/control", { ...body, session: currentSeat });
expect(current.status).toBe(201);
expect(decisions.create).toHaveBeenCalledWith(expect.objectContaining({
sessionName: currentSeat,
generationUuid: sessionRegistry.currentOccupantGenerationForSession(currentSeat),
direction,
}));
},
);
it("the public manual trigger honors a current hold, records observation, and sends nothing", async () => {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
const hold = store.create({
enforcerKind: "claude_compaction",
sessionName: SEAT,
generationUuid,
direction: "hold",
automaticReason: null,
reason: "finish the current atomic action",
actorSession: IDENTITY,
identityProvenance: "transport:v1",
});
const send = vi.fn(async () => ({ ok: true as const }));
const transport = {
send,
resolveSessions: vi.fn(async () => ({ ok: true as const, sessions: [SEAT] })),
};
const enforcer = new ClaudeCompactionEnforcer(
{
resolveClaudeCompactionPolicy: () => ({
enabled: true,
thresholdPercent: 80,
preCompactInstruction: "prepare",
compactInstruction: "",
messageInline: "",
messageFilePath: "",
postRestoreAuditInstruction: "audit",
authorizeTtlMinutes: 15,
}),
} as never,
transport as never,
{
resolveOccupantGeneration: (sessionName: string) =>
sessionRegistry.currentOccupantGenerationForSession(sessionName),
decisionStore: store,
},
);
manualDeps = {
enforcer,
transport,
usageStore: {
getForNode: () => ({
availability: "known",
usedPercentage: 90,
transcriptPath: "/tmp/claude.jsonl",
sessionId: "claude-session",
}),
},
};
decisions = store as never;
const response = await request("/api/compaction/trigger", { session: SEAT });
expect(response.status).toBe(409);
expect(await response.json()).toMatchObject({
ok: false,
reason: "human_hold",
decisionId: hold.decisionId,
});
expect(send).not.toHaveBeenCalled();
expect(store.list({ sessionName: SEAT })).toContainEqual(expect.objectContaining({
decisionId: hold.decisionId,
lastObservedOutcome: "human_hold",
lastObservedAt: "2026-08-08T18:00:00.000Z",
}));
});
it("the public manual trigger rejects a historical alias before it can miss the current alias hold", async () => {
const currentSeat = "claude-seat-next@rig";
sessionRegistry.registerSession(nodeId, currentSeat);
const currentGeneration = sessionRegistry.currentOccupantGenerationForSession(currentSeat)!;
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
const hold = store.create({
enforcerKind: "claude_compaction",
sessionName: currentSeat,
generationUuid: currentGeneration,
direction: "hold",
automaticReason: null,
reason: "finish the current atomic action",
actorSession: IDENTITY,
identityProvenance: "transport:v1",
});
const send = vi.fn(async () => ({ ok: true as const }));
const transport = {
send,
resolveSessions: vi.fn(async () => ({ ok: true as const, sessions: [SEAT] })),
};
const enforcer = new ClaudeCompactionEnforcer(
{
resolveClaudeCompactionPolicy: () => ({
enabled: true,
thresholdPercent: 80,
preCompactInstruction: "prepare",
compactInstruction: "",
messageInline: "",
messageFilePath: "",
postRestoreAuditInstruction: "audit",
authorizeTtlMinutes: 15,
}),
} as never,
transport as never,
{
resolveOccupantGeneration: (sessionName: string) =>
sessionRegistry.currentOccupantGenerationForSession(sessionName),
decisionStore: store,
},
);
manualDeps = {
enforcer,
transport,
usageStore: {
getForNode: () => ({
availability: "known",
usedPercentage: 90,
transcriptPath: "/tmp/claude.jsonl",
sessionId: "claude-session",
}),
},
};
decisions = store as never;
const stale = await request("/api/compaction/trigger", { session: SEAT });
expect(stale.status).toBe(409);
expect(await stale.json()).toMatchObject({
ok: false,
error: "session_not_current",
currentSession: currentSeat,
});
expect(send).not.toHaveBeenCalled();
expect(store.list({ sessionName: currentSeat })).toContainEqual(expect.objectContaining({
decisionId: hold.decisionId,
active: true,
lastObservedAt: null,
}));
const current = await request("/api/compaction/trigger", { session: currentSeat });
expect(current.status).toBe(409);
expect(await current.json()).toMatchObject({
ok: false,
reason: "human_hold",
decisionId: hold.decisionId,
});
expect(send).not.toHaveBeenCalled();
expect(store.list({ sessionName: currentSeat })).toContainEqual(expect.objectContaining({
decisionId: hold.decisionId,
lastObservedOutcome: "human_hold",
}));
});
it("the public manual trigger observes a new-current-alias hold activated during preparation", async () => {
const currentSeat = "claude-seat-next@rig";
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
let hold: ReturnType<EnforcerDecisionStore["create"]> | null = null;
const tx = makeBoundaryTransport(() => {
sessionRegistry.registerSession(nodeId, currentSeat, "handover");
hold = store.create({
enforcerKind: "claude_compaction",
sessionName: currentSeat,
generationUuid: sessionRegistry.currentOccupantGenerationForSession(currentSeat)!,
direction: "hold",
automaticReason: null,
reason: "hold the new occupant",
actorSession: IDENTITY,
identityProvenance: "transport:v1",
});
});
installManualHarness(store, tx.transport);
const response = await request("/api/compaction/trigger", { session: SEAT });
expect(response.status).toBe(409);
expect(await response.json()).toMatchObject({
ok: false,
reason: "human_hold",
decisionId: hold?.decisionId,
});
expect(tx.delivered).toHaveLength(1);
expect(tx.delivered[0]).toContain("automatic compaction preparation");
expect(store.list({ sessionName: currentSeat })).toContainEqual(expect.objectContaining({
decisionId: hold?.decisionId,
lastObservedOutcome: "human_hold",
lastObservedAt: "2026-08-08T18:00:00.000Z",
}));
});
it("the public manual trigger refuses an alias turnover without a hold at the final boundary", async () => {
const currentSeat = "claude-seat-next@rig";
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
const tx = makeBoundaryTransport(() => {
sessionRegistry.registerSession(nodeId, currentSeat, "handover");
});
installManualHarness(store, tx.transport);
const response = await request("/api/compaction/trigger", { session: SEAT });
expect(response.status).toBe(409);
expect(await response.json()).toMatchObject({
ok: false,
reason: "session_not_current",
});
expect(tx.delivered).toHaveLength(1);
});
it("the public manual trigger refuses same-alias occupant-tenure drift at the final boundary", async () => {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
const tx = makeBoundaryTransport(() => {
sessionRegistry.mintOccupantTenure(nodeId, "handover");
});
installManualHarness(store, tx.transport);
const response = await request("/api/compaction/trigger", { session: SEAT });
expect(response.status).toBe(409);
expect(await response.json()).toMatchObject({
ok: false,
reason: "occupant_generation_changed",
});
expect(tx.delivered).toHaveLength(1);
});
it("the public manual trigger refuses a same-alias handover when successor tenure minting fails", async () => {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
const startingGeneration = sessionRegistry.currentOccupantGenerationForSession(SEAT);
let invalidator: DefaultOccupantInvalidator;
const tx = makeBoundaryTransport(() => {
const mint = vi.spyOn(sessionRegistry, "mintOccupantTenure").mockImplementationOnce(() => {
throw new Error("forced successor tenure mint failure");
});
sessionRegistry.registerClaimedSession(nodeId, SEAT, "handover");
mint.mockRestore();
expect(sessionRegistry.currentOccupantGenerationForSession(SEAT)).toBe(startingGeneration);
expect(
(db.prepare("SELECT session_name FROM sessions WHERE node_id = ? ORDER BY id DESC LIMIT 1")
.get(nodeId) as { session_name: string }).session_name,
).toBe(SEAT);
invalidator.invalidateRetiringOccupant({
retiringSessionName: SEAT,
successorSessionName: SEAT,
retiringGeneration: startingGeneration ?? undefined,
});
});
installManualHarness(store, tx.transport);
invalidator = new DefaultOccupantInvalidator({
enforcer: manualDeps!.enforcer,
contextUsage: { invalidateOccupantSidecar: vi.fn() },
});
const response = await request("/api/compaction/trigger", { session: SEAT });
expect(response.status).toBe(409);
expect(await response.json()).toMatchObject({
ok: false,
reason: "occupant_generation_changed",
});
expect(tx.delivered).toHaveLength(1);
expect(tx.delivered[0]).toContain("automatic compaction preparation");
});
it("lists durable observation fields without requiring a write identity", async () => {
const response = await request(`/api/compaction/control?session=${encodeURIComponent(SEAT)}`);
expect(response.status).toBe(200);
expect(await response.json()).toEqual({
ok: true,
decisions: [{
decisionId: "hold-1",
sessionName: SEAT,
direction: "hold",
active: true,
lastObservedAt: "2026-08-08T18:00:30.000Z",
lastObservedOutcome: "human_hold",
}],
});
expect(decisions.list).toHaveBeenCalledWith({ sessionName: SEAT });
});
it("clear derives its human actor and records a required release reason", async () => {
const response = await request("/api/compaction/control/hold-1/clear", {
reason: "atomic action completed",
});
expect(response.status).toBe(200);
expect(decisions.clear).toHaveBeenCalledWith({
decisionId: "hold-1",
actorSession: IDENTITY,
identityProvenance: "transport:v1",
reason: "atomic action completed",
});
});
});
@@ -1,22 +0,0 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
const here = dirname(fileURLToPath(import.meta.url));
describe("W4 production wiring", () => {
it("constructs one decision store and shares it with the enforcer and routes", () => {
const startup = readFileSync(join(here, "../src/startup.ts"), "utf8");
const server = readFileSync(join(here, "../src/server.ts"), "utf8");
expect(startup).toContain('import("./domain/enforcer-decision-store.js")');
expect(startup.match(/new EnforcerDecisionStore\(/g)).toHaveLength(1);
expect(startup).toMatch(/decisionStore:\s*enforcerDecisionStore/);
expect(startup).toMatch(/deps\.enforcerDecisionStore\s*=\s*enforcerDecisionStore/);
expect(server).toMatch(/enforcerDecisionStore\?:\s*import\("\.\/domain\/enforcer-decision-store\.js"\)\.EnforcerDecisionStore/);
expect(server).toContain('c.set("enforcerDecisionStore" as never, deps.enforcerDecisionStore)');
expect(server).toContain("compactionRoutes");
});
});
@@ -1,866 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import type Database from "better-sqlite3";
import { createDb } from "../src/db/connection.js";
import { migrate } from "../src/db/migrate.js";
import { ALL_MIGRATIONS } from "../src/db/all-migrations.js";
import {
ClaudeCompactionEnforcer,
type EnforcerInput,
} from "../src/domain/claude-compaction-enforcer.js";
import type { SessionTransport } from "../src/domain/session-transport.js";
const STORE_MODULE = ["../src/domain", "enforcer-decision-store.js"].join("/");
const KIND = "claude_compaction";
const SEAT = "claude-seat@rig";
type DecisionStoreConstructor = new (
db: Database.Database,
opts: { now: () => Date; authorizeTtlMinutes: () => number },
) => {
create(input: Record<string, unknown>): Record<string, unknown>;
list(input?: Record<string, unknown>): Array<Record<string, unknown>>;
clear(input: Record<string, unknown>): Record<string, unknown>;
findActiveHold(input: Record<string, unknown>): Record<string, unknown> | null;
consumeAuthorizationForAttempt(input: Record<string, unknown>): boolean;
recordAuthorizationAttempt(input: Record<string, unknown>): void;
};
async function loadDecisionStore(): Promise<DecisionStoreConstructor> {
let mod: { EnforcerDecisionStore?: DecisionStoreConstructor } | null = null;
try {
mod = await import(/* @vite-ignore */ STORE_MODULE);
} catch {
// Assertion below keeps the RED feature-shaped instead of surfacing a loader error.
}
expect(mod, "durable enforcer decision store module must exist").not.toBeNull();
expect(mod?.EnforcerDecisionStore).toBeTypeOf("function");
return mod!.EnforcerDecisionStore!;
}
function makeDb(): Database.Database {
const db = createDb();
migrate(db, ALL_MIGRATIONS);
return db;
}
function humanInput(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
enforcerKind: KIND,
sessionName: SEAT,
generationUuid: "gen-1",
direction: "hold",
reason: "seat is inside a load-bearing atomic step",
actorSession: "orch-lead@rig",
identityProvenance: "transport:v1",
...overrides,
};
}
describe("W4 durable enforcer decision store", () => {
it("registers migration 068 and creates the durable decision table", () => {
expect(ALL_MIGRATIONS.map((migration) => migration.name)).toContain(
"068_enforcer_decisions.sql",
);
const db = makeDb();
try {
const row = db.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'enforcer_decisions'",
).get() as { name: string } | undefined;
expect(row?.name).toBe("enforcer_decisions");
} finally {
db.close();
}
});
it("persists an attributable generation-scoped hold with enforcer kind", async () => {
const EnforcerDecisionStore = await loadDecisionStore();
const db = makeDb();
try {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
const created = store.create(humanInput());
expect(created).toMatchObject({
enforcerKind: KIND,
sessionName: SEAT,
generationUuid: "gen-1",
direction: "hold",
actorSession: "orch-lead@rig",
identityProvenance: "transport:v1",
reason: "seat is inside a load-bearing atomic step",
active: true,
});
} finally {
db.close();
}
});
it("makes authorize expiring, reason-matched, and limited to the locked lift set", async () => {
const EnforcerDecisionStore = await loadDecisionStore();
const db = makeDb();
try {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
for (const [index, automaticReason] of [
"disabled",
"post_restore_cooldown",
"stale_generation",
].entries()) {
const created = store.create(humanInput({
generationUuid: `gen-${index + 1}`,
direction: "authorize",
automaticReason,
reason: `lift ${automaticReason} once`,
}));
expect(created).toMatchObject({
direction: "authorize",
automaticReason,
expiresAt: "2026-08-08T18:15:00.000Z",
active: true,
});
}
for (const automaticReason of [
"runtime_filter",
"no_usage_data",
"invalid_policy",
"below_threshold",
"already_triggered_above_threshold",
"dedup_window",
"send_failed",
"human_hold",
]) {
expect(() => store.create(humanInput({
generationUuid: `excluded-${automaticReason}`,
direction: "authorize",
automaticReason,
reason: "must be refused",
}))).toThrow(/automatic reason|not authorizable|liftable/i);
}
} finally {
db.close();
}
});
it("enforces one active decision and resolves conflicts toward hold", async () => {
const EnforcerDecisionStore = await loadDecisionStore();
const db = makeDb();
try {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
const authorization = store.create(humanInput({
direction: "authorize",
automaticReason: "disabled",
reason: "one disabled-policy attempt",
}));
const hold = store.create(humanInput({ reason: "new work began; stop before acting" }));
expect(hold).toMatchObject({ direction: "hold", active: true });
const rows = store.list({ sessionName: SEAT });
expect(rows.find((row) => row["decisionId"] === authorization["decisionId"])).toMatchObject({
active: false,
releaseKind: "revoked_by_hold",
releasedBySession: "orch-lead@rig",
releaseIdentityProvenance: "transport:v1",
});
expect(() => store.create(humanInput({
direction: "authorize",
automaticReason: "disabled",
reason: "must not outrank hold",
}))).toThrow(/active hold/i);
} finally {
db.close();
}
});
it("consumes one authorization atomically for one machine send attempt", async () => {
const EnforcerDecisionStore = await loadDecisionStore();
const db = makeDb();
try {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
const created = store.create(humanInput({
direction: "authorize",
automaticReason: "disabled",
reason: "allow one disabled-policy attempt",
}));
const consumeInput = {
decisionId: created["decisionId"],
enforcerKind: KIND,
liftedReason: "disabled",
};
expect(store.consumeAuthorizationForAttempt(consumeInput)).toBe(true);
expect(store.consumeAuthorizationForAttempt(consumeInput)).toBe(false);
store.recordAuthorizationAttempt({
decisionId: created["decisionId"],
outcome: "failed",
failureReason: "send_failed",
});
expect(store.list({ sessionName: SEAT })[0]).toMatchObject({
consumedByEnforcerKind: KIND,
liftedReason: "disabled",
attemptOutcome: "failed",
attemptFailureReason: "send_failed",
});
expect(store.list({ sessionName: SEAT })[0]).not.toHaveProperty("consumedByActorSession");
expect(store.list({ sessionName: SEAT })[0]).not.toHaveProperty("consumeIdentityProvenance");
} finally {
db.close();
}
});
it("records explicit human clear and lazy expiry without erasing history", async () => {
const EnforcerDecisionStore = await loadDecisionStore();
const db = makeDb();
let now = new Date("2026-08-08T18:00:00.000Z");
try {
const store = new EnforcerDecisionStore(db, {
now: () => now,
authorizeTtlMinutes: () => 15,
});
const hold = store.create(humanInput());
store.clear({
decisionId: hold["decisionId"],
actorSession: "orch-lead@rig",
identityProvenance: "transport:v1",
reason: "atomic step completed",
});
expect(store.list({ sessionName: SEAT })[0]).toMatchObject({
active: false,
releaseKind: "cleared",
releasedBySession: "orch-lead@rig",
releaseIdentityProvenance: "transport:v1",
releaseReason: "atomic step completed",
});
store.create(humanInput({
generationUuid: "gen-2",
direction: "authorize",
automaticReason: "disabled",
reason: "short-lived one-shot",
}));
now = new Date("2026-08-08T18:16:00.000Z");
const expired = store.list({ sessionName: SEAT }).find((row) => row["generationUuid"] === "gen-2");
expect(expired).toMatchObject({ active: false, releaseKind: "expired" });
} finally {
db.close();
}
});
it("refuses anonymous, reasonless, and blanket decisions loudly", async () => {
const EnforcerDecisionStore = await loadDecisionStore();
const db = makeDb();
try {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
expect(() => store.create(humanInput({ actorSession: "" }))).toThrow(/actor|required|attribut/i);
expect(() => store.create(humanInput({ reason: " " }))).toThrow(/reason|required/i);
expect(() => store.create(humanInput({
generationUuid: "gen-blanket",
direction: "authorize",
automaticReason: undefined,
reason: "ignore refusals",
}))).toThrow(/automatic reason|required|blanket/i);
} finally {
db.close();
}
});
});
const BASE_POLICY = {
enabled: true,
thresholdPercent: 80,
preCompactInstruction: "prepare",
compactInstruction: "",
messageInline: "",
messageFilePath: "",
postRestoreAuditInstruction: "audit",
authorizeTtlMinutes: 15,
};
function settings(policy: Record<string, unknown> = BASE_POLICY) {
return { resolveClaudeCompactionPolicy: () => policy } as never;
}
function transport() {
const send = vi.fn(async () => ({ ok: true as const }));
return { send, value: { send } as unknown as SessionTransport };
}
function decisionHarness(opts: {
hold?: (generation: string | null) => Record<string, unknown> | null;
authorizationReason?: string;
} = {}) {
const findActiveHold = vi.fn((input: { liveGenerationUuid: string | null }) =>
opts.hold?.(input.liveGenerationUuid) ?? null);
const findMatchingAuthorization = vi.fn((input: { automaticReason: string }) =>
input.automaticReason === opts.authorizationReason
? { decisionId: "authorize-1", automaticReason: input.automaticReason }
: null);
return {
findActiveHold,
findMatchingAuthorization,
observeHold: vi.fn(),
consumeAuthorizationForAttempt: vi.fn(() => true),
recordAuthorizationAttempt: vi.fn(),
};
}
const highInput = (usedPercentage = 90): EnforcerInput => ({
sessionName: SEAT,
runtime: "claude-code",
usedPercentage,
transcriptPath: "/tmp/claude.jsonl",
});
describe("W4 ClaudeCompactionEnforcer decision consumption", () => {
it("a hold prevents firing and reports human_hold durably", async () => {
const tx = transport();
const decisions = decisionHarness({ hold: () => ({ decisionId: "hold-1" }) });
const enforcer = new ClaudeCompactionEnforcer(settings(), tx.value, {
resolveOccupantGeneration: () => "gen-1",
decisionStore: decisions,
} as never);
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: false,
reason: "human_hold",
decisionId: "hold-1",
});
expect(tx.send).not.toHaveBeenCalled();
expect(decisions.observeHold).toHaveBeenCalledWith(expect.objectContaining({
decisionId: "hold-1",
outcome: "human_hold",
}));
});
it("a null live generation fails closed on a hold, then known N+1 makes the N hold inert", async () => {
const tx = transport();
let liveGeneration: string | null = null;
const decisions = decisionHarness({
hold: (generation) => generation === null ? { decisionId: "hold-N" } : null,
});
const enforcer = new ClaudeCompactionEnforcer(settings(), tx.value, {
resolveOccupantGeneration: () => liveGeneration,
decisionStore: decisions,
} as never);
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: false,
reason: "human_hold",
decisionId: "hold-N",
});
liveGeneration = "gen-N+1";
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({ triggered: true });
expect(tx.send).toHaveBeenCalledTimes(1);
});
it("matches a persisted generation-N hold and makes it inert for generation N+1", async () => {
const EnforcerDecisionStore = await loadDecisionStore();
const db = makeDb();
try {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
const hold = store.create(humanInput({ generationUuid: "gen-N" }));
const tx = transport();
let liveGeneration = "gen-N";
const enforcer = new ClaudeCompactionEnforcer(settings(), tx.value, {
resolveOccupantGeneration: () => liveGeneration,
decisionStore: store,
} as never);
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: false,
reason: "human_hold",
decisionId: hold["decisionId"],
});
expect(tx.send).not.toHaveBeenCalled();
liveGeneration = "gen-N+1";
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({ triggered: true });
expect(tx.send).toHaveBeenCalledTimes(1);
} finally {
db.close();
}
});
it("rechecks the real hold store after manual preparation and before the final action", async () => {
const EnforcerDecisionStore = await loadDecisionStore();
const db = makeDb();
try {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
let hold: Record<string, unknown> | null = null;
const delivered: string[] = [];
const send = vi.fn(async (
_sessionName: string,
message: string,
opts?: { beforeSend?: () => Promise<{ reason: string; error?: string } | null> },
) => {
if (send.mock.calls.length === 1) {
hold = store.create(humanInput({
generationUuid: "gen-N",
reason: "hold activated during manual preparation",
}));
}
const refusal = await opts?.beforeSend?.();
if (refusal) return { ok: false as const, sessionName: SEAT, sent: false, ...refusal };
delivered.push(message);
return { ok: true as const };
});
const enforcer = new ClaudeCompactionEnforcer(settings(), { send } as never, {
resolveOccupantGeneration: () => "gen-N",
decisionStore: store,
} as never);
await expect(enforcer.triggerManualCompact(highInput(), { operatorInitiated: true })).resolves.toEqual({
triggered: false,
stage: "skipped-or-failed",
reason: "human_hold",
decisionId: expect.any(String),
});
expect(send).toHaveBeenCalledTimes(2);
expect(delivered).toHaveLength(1);
expect(delivered[0]).toContain("automatic compaction preparation");
expect(hold).not.toBeNull();
expect(store.list({ sessionName: SEAT })).toContainEqual(expect.objectContaining({
decisionId: hold?.["decisionId"],
active: true,
lastObservedOutcome: "human_hold",
lastObservedAt: "2026-08-08T18:00:00.000Z",
}));
} finally {
db.close();
}
});
it("consumes a persisted authorization only for matching known generation N and only once", async () => {
const EnforcerDecisionStore = await loadDecisionStore();
const db = makeDb();
try {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
const authorization = store.create(humanInput({
generationUuid: "gen-N",
direction: "authorize",
automaticReason: "disabled",
reason: "lift disabled once for generation N",
}));
const row = () => store.list({ sessionName: SEAT }).find(
(candidate) => candidate["decisionId"] === authorization["decisionId"],
);
const tx = transport();
let liveGeneration: string | null = null;
const enforcer = new ClaudeCompactionEnforcer(
settings({ ...BASE_POLICY, enabled: false }),
tx.value,
{
resolveOccupantGeneration: () => liveGeneration,
decisionStore: store,
} as never,
);
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: false,
reason: "disabled",
});
expect(row()).toMatchObject({ active: true });
expect(row()?.["consumedAt"] ?? null).toBeNull();
expect(tx.send).not.toHaveBeenCalled();
liveGeneration = "gen-N+1";
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: false,
reason: "disabled",
});
expect(row()).toMatchObject({ active: true });
expect(row()?.["consumedAt"] ?? null).toBeNull();
expect(tx.send).not.toHaveBeenCalled();
liveGeneration = "gen-N";
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: true,
decisionId: authorization["decisionId"],
liftedReason: "disabled",
});
expect(row()).toMatchObject({ active: true });
expect(row()?.["consumedAt"] ?? null).toBeNull();
expect(tx.send).toHaveBeenCalledTimes(1);
expect(tx.send.mock.calls[0]?.[1]).toContain("automatic compaction preparation");
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: true,
decisionId: authorization["decisionId"],
liftedReason: "disabled",
});
expect(row()).toMatchObject({
active: false,
consumedByEnforcerKind: KIND,
liftedReason: "disabled",
attemptOutcome: "succeeded",
});
expect(tx.send).toHaveBeenCalledTimes(2);
expect(tx.send.mock.calls[1]?.[1]).toMatch(/^\/compact/);
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: false,
reason: "disabled",
});
expect(tx.send).toHaveBeenCalledTimes(2);
} finally {
db.close();
}
});
it("lifts disabled once and reports decisionId + liftedReason on the triggered outcome", async () => {
const tx = transport();
const decisions = decisionHarness({ authorizationReason: "disabled" });
const enforcer = new ClaudeCompactionEnforcer(
settings({ ...BASE_POLICY, enabled: false }),
tx.value,
{
resolveOccupantGeneration: () => "gen-1",
decisionStore: decisions,
} as never,
);
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: true,
decisionId: "authorize-1",
liftedReason: "disabled",
});
expect(tx.send).toHaveBeenCalledTimes(1);
expect(decisions.consumeAuthorizationForAttempt).not.toHaveBeenCalled();
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: true,
decisionId: "authorize-1",
liftedReason: "disabled",
});
expect(tx.send).toHaveBeenCalledTimes(2);
expect(decisions.consumeAuthorizationForAttempt).toHaveBeenCalledTimes(1);
expect(decisions.recordAuthorizationAttempt).toHaveBeenCalledWith(expect.objectContaining({
decisionId: "authorize-1",
outcome: "succeeded",
}));
});
it("does not consume authorization when its reason is not the refusal that releases a send", async () => {
const tx = transport();
const decisions = decisionHarness({ authorizationReason: "disabled" });
const enforcer = new ClaudeCompactionEnforcer(settings(), tx.value, {
resolveOccupantGeneration: () => "gen-1",
decisionStore: decisions,
} as never);
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({ triggered: true });
expect(tx.send).toHaveBeenCalledTimes(1);
expect(decisions.consumeAuthorizationForAttempt).not.toHaveBeenCalled();
});
it("lifts post_restore_cooldown for one send attempt", async () => {
const tx = transport();
const decisions = decisionHarness({ authorizationReason: "post_restore_cooldown" });
let now = 1_700_000_000_000;
vi.spyOn(Date, "now").mockImplementation(() => now);
try {
const enforcer = new ClaudeCompactionEnforcer(settings(), tx.value, {
dedupWindowMs: 0,
postCompactRestoreCooldownMs: 60_000,
openrigHome: "/tmp/openrig-w4",
resolveOccupantGeneration: () => "gen-1",
decisionStore: decisions,
} as never);
await enforcer.maybeAutoCompact(highInput());
now += 1_000;
await enforcer.maybeAutoCompact(highInput());
for (let i = 0; i < 3; i += 1) {
now += 1_000;
await enforcer.maybeAutoCompact(highInput(20));
}
now += 1_000;
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: true,
decisionId: "authorize-1",
liftedReason: "post_restore_cooldown",
});
expect(decisions.consumeAuthorizationForAttempt).not.toHaveBeenCalled();
now += 1_000;
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: true,
decisionId: "authorize-1",
liftedReason: "post_restore_cooldown",
});
expect(decisions.consumeAuthorizationForAttempt).toHaveBeenCalledTimes(1);
} finally {
vi.restoreAllMocks();
}
});
it("checks stale_generation authorization before deleting the queued stage", async () => {
const tx = transport();
const decisions = decisionHarness({ authorizationReason: "stale_generation" });
let generation = "gen-N";
const enforcer = new ClaudeCompactionEnforcer(settings(), tx.value, {
dedupWindowMs: 0,
postCompactRestoreCooldownMs: 0,
openrigHome: "/tmp/openrig-w4",
resolveOccupantGeneration: () => generation,
decisionStore: decisions,
} as never);
await enforcer.maybeAutoCompact(highInput());
await enforcer.maybeAutoCompact(highInput());
generation = "gen-N+1";
await expect(enforcer.maybeAutoCompact(highInput(20))).resolves.toEqual({
triggered: true,
decisionId: "authorize-1",
liftedReason: "stale_generation",
});
expect(tx.send.mock.calls.at(-1)?.[1]).toContain("post-compaction turn boundary");
expect(decisions.consumeAuthorizationForAttempt).toHaveBeenCalledTimes(1);
});
it("carries one real stale_generation authorization through the inherited restore occurrence", async () => {
const EnforcerDecisionStore = await loadDecisionStore();
const db = makeDb();
try {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
const tx = transport();
let generation = "gen-N";
const enforcer = new ClaudeCompactionEnforcer(settings(), tx.value, {
dedupWindowMs: 0,
postCompactRestoreCooldownMs: 0,
openrigHome: "/tmp/openrig-w4",
resolveOccupantGeneration: () => generation,
decisionStore: store,
} as never);
await enforcer.maybeAutoCompact(highInput());
await enforcer.maybeAutoCompact(highInput());
generation = "gen-N+1";
const authorization = store.create(humanInput({
generationUuid: generation,
direction: "authorize",
automaticReason: "stale_generation",
reason: "allow the inherited restore occurrence once",
}));
const lifted = {
triggered: true,
decisionId: authorization["decisionId"],
liftedReason: "stale_generation",
};
await expect(enforcer.maybeAutoCompact(highInput(20))).resolves.toEqual(lifted);
await expect(enforcer.maybeAutoCompact(highInput(20))).resolves.toEqual(lifted);
await expect(enforcer.maybeAutoCompact(highInput(20))).resolves.toEqual(lifted);
expect(tx.send).toHaveBeenCalledTimes(5);
expect(tx.send.mock.calls[2]?.[1]).toContain("post-compaction turn boundary");
expect(tx.send.mock.calls[3]?.[1]).toContain("restoring this Claude session");
expect(tx.send.mock.calls[4]?.[1]).toContain("audit your compaction restore");
expect(store.list({ sessionName: SEAT })).toContainEqual(expect.objectContaining({
decisionId: authorization["decisionId"],
active: false,
consumedByEnforcerKind: KIND,
liftedReason: "stale_generation",
attemptOutcome: "succeeded",
}));
await expect(enforcer.maybeAutoCompact(highInput(20))).resolves.toEqual({
triggered: false,
reason: "below_threshold",
});
expect(tx.send).toHaveBeenCalledTimes(5);
} finally {
db.close();
}
});
it.each([
{
failure: "result-false",
failNextSend: (send: ReturnType<typeof vi.fn>) => send.mockResolvedValueOnce({
ok: false as const,
reason: "boundary refused",
}),
failureReason: "boundary refused",
},
{
failure: "thrown",
failNextSend: (send: ReturnType<typeof vi.fn>) => send.mockRejectedValueOnce(
new Error("boundary exploded"),
),
failureReason: "boundary exploded",
},
])("clears a failed real stale_generation carry after a $failure send", async ({
failNextSend,
failureReason,
}) => {
const EnforcerDecisionStore = await loadDecisionStore();
const db = makeDb();
try {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
const tx = transport();
let generation = "gen-N";
const enforcer = new ClaudeCompactionEnforcer(settings(), tx.value, {
dedupWindowMs: 0,
postCompactRestoreCooldownMs: 0,
openrigHome: "/tmp/openrig-w4",
resolveOccupantGeneration: () => generation,
decisionStore: store,
} as never);
await enforcer.maybeAutoCompact(highInput());
await enforcer.maybeAutoCompact(highInput());
generation = "gen-N+1";
const authorization = store.create(humanInput({
generationUuid: generation,
direction: "authorize",
automaticReason: "stale_generation",
reason: "allow one inherited boundary attempt",
}));
failNextSend(tx.send);
await expect(enforcer.maybeAutoCompact(highInput(20))).resolves.toEqual({
triggered: false,
reason: "send_failed",
});
expect(tx.send).toHaveBeenCalledTimes(3);
expect(store.list({ sessionName: SEAT })).toContainEqual(expect.objectContaining({
decisionId: authorization["decisionId"],
active: false,
consumedByEnforcerKind: KIND,
liftedReason: "stale_generation",
attemptOutcome: "failed",
attemptFailureReason: failureReason,
}));
await expect(enforcer.maybeAutoCompact(highInput(20))).resolves.toEqual({
triggered: false,
reason: "stale_generation",
});
await expect(enforcer.maybeAutoCompact(highInput(20))).resolves.toEqual({
triggered: false,
reason: "below_threshold",
});
expect(tx.send).toHaveBeenCalledTimes(3);
} finally {
db.close();
}
});
it("consumes authorization on a failed compact attempt and records the actual failure", async () => {
const send = vi.fn()
.mockResolvedValueOnce({ ok: true as const })
.mockResolvedValueOnce({ ok: false as const, reason: "send_failed" });
const decisions = decisionHarness({ authorizationReason: "disabled" });
const enforcer = new ClaudeCompactionEnforcer(
settings({ ...BASE_POLICY, enabled: false }),
{ send } as unknown as SessionTransport,
{
resolveOccupantGeneration: () => "gen-1",
decisionStore: decisions,
} as never,
);
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: true,
decisionId: "authorize-1",
liftedReason: "disabled",
});
expect(decisions.consumeAuthorizationForAttempt).not.toHaveBeenCalled();
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: false,
reason: "send_failed",
});
expect(decisions.consumeAuthorizationForAttempt).toHaveBeenCalledTimes(1);
expect(decisions.recordAuthorizationAttempt).toHaveBeenCalledWith(expect.objectContaining({
decisionId: "authorize-1",
outcome: "failed",
failureReason: "send_failed",
}));
});
it("records a thrown authorized send as failed and keeps the authorization one-shot", async () => {
const EnforcerDecisionStore = await loadDecisionStore();
const db = makeDb();
try {
const store = new EnforcerDecisionStore(db, {
now: () => new Date("2026-08-08T18:00:00.000Z"),
authorizeTtlMinutes: () => 15,
});
const authorization = store.create(humanInput({
generationUuid: "gen-N",
direction: "authorize",
automaticReason: "disabled",
reason: "allow one attempted send",
}));
const send = vi.fn()
.mockResolvedValueOnce({ ok: true as const })
.mockRejectedValueOnce(new Error("transport exploded"));
const enforcer = new ClaudeCompactionEnforcer(
settings({ ...BASE_POLICY, enabled: false }),
{ send } as unknown as SessionTransport,
{
resolveOccupantGeneration: () => "gen-N",
decisionStore: store,
} as never,
);
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: true,
decisionId: authorization["decisionId"],
liftedReason: "disabled",
});
expect(store.list({ sessionName: SEAT })).toContainEqual(expect.objectContaining({
decisionId: authorization["decisionId"],
active: true,
consumedAt: null,
}));
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: false,
reason: "send_failed",
});
expect(store.list({ sessionName: SEAT })).toContainEqual(expect.objectContaining({
decisionId: authorization["decisionId"],
active: false,
consumedByEnforcerKind: KIND,
liftedReason: "disabled",
attemptOutcome: "failed",
attemptFailureReason: "transport exploded",
}));
await expect(enforcer.maybeAutoCompact(highInput())).resolves.toEqual({
triggered: false,
reason: "disabled",
});
expect(send).toHaveBeenCalledTimes(2);
} finally {
db.close();
}
});
});
+4 -105
View File
@@ -4,11 +4,9 @@ import { execFileSync, spawn, spawnSync } from "node:child_process";
import {
chmodSync,
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
readlinkSync,
symlinkSync,
writeFileSync,
} from "node:fs";
@@ -77,14 +75,6 @@ function assertCandidateRefusal(result, pattern) {
assert.match(`${result.stdout}\n${result.stderr}`, pattern);
}
function symlinkMatches(path, target) {
try {
return lstatSync(path).isSymbolicLink() && readlinkSync(path) === target;
} catch {
return false;
}
}
test("candidate binding: tracked dirt refuses before the first cleanup mutation", async () => {
const root = makeRepo();
const sentinel = join(root, "packages/cli/daemon/sentinel.txt");
@@ -107,101 +97,10 @@ test("candidate binding: same verdict basename at a different path is not exclud
assertCandidateRefusal(await runGate(root), /sub\/dir\/gate-lane-verdict\.json|candidate.*dirty/i);
});
test("candidate binding: a tracked verdict destination is refused without replacing owner data", async () => {
const root = makeRepo();
const verdictPath = join(root, "tracked.txt");
const ownerData = readFileSync(verdictPath, "utf8");
const result = await runGate(root, { OPENRIG_GATE_VERDICT: verdictPath });
assertCandidateRefusal(result, /verdict.*tracked|tracked.*verdict/i);
assert.equal(readFileSync(verdictPath, "utf8"), ownerData);
});
test("candidate binding: a tracked symlink verdict destination is refused without replacing owner data", async () => {
const root = makeRepo();
const ownerPath = join(root, "receipts/owner.json");
const ownerData = "contained owner data\n";
const verdictPath = join(root, "tracked-verdict.json");
const linkTarget = "receipts/owner.json";
write(ownerPath, ownerData);
symlinkSync(linkTarget, verdictPath);
git(root, "add", "tracked-verdict.json");
git(root, "commit", "-qm", "track verdict symlink");
const result = await runGate(root, { OPENRIG_GATE_VERDICT: verdictPath });
assert.deepEqual(
{
refused: result.status !== 0,
ownerPreserved: existsSync(ownerPath) && readFileSync(ownerPath, "utf8") === ownerData,
linkPreserved: symlinkMatches(verdictPath, linkTarget),
},
{ refused: true, ownerPreserved: true, linkPreserved: true },
result.stderr,
);
});
test("candidate binding: a tracked symlink parent is refused without replacing owner data", async () => {
const root = makeRepo();
const ownerPath = join(root, "owner-receipts/current-verdict.json");
const ownerData = "contained parent owner data\n";
const linkPath = join(root, "tracked-receipts");
const linkTarget = "owner-receipts";
write(ownerPath, ownerData);
symlinkSync(linkTarget, linkPath);
git(root, "add", "tracked-receipts");
git(root, "commit", "-qm", "track verdict parent symlink");
const result = await runGate(root, { OPENRIG_GATE_VERDICT: join(linkPath, "current-verdict.json") });
assert.deepEqual(
{
refused: result.status !== 0,
ownerPreserved: existsSync(ownerPath) && readFileSync(ownerPath, "utf8") === ownerData,
linkPreserved: symlinkMatches(linkPath, linkTarget),
},
{ refused: true, ownerPreserved: true, linkPreserved: true },
result.stderr,
);
});
test("candidate binding: Git metadata is refused without replacing owner data", async () => {
const root = makeRepo();
const verdictPath = join(root, ".git/config");
const ownerData = readFileSync(verdictPath, "utf8");
const result = await runGate(root, { OPENRIG_GATE_VERDICT: verdictPath });
assert.deepEqual(
{
refused: result.status !== 0,
ownerPreserved: existsSync(verdictPath) && readFileSync(verdictPath, "utf8") === ownerData,
},
{ refused: true, ownerPreserved: true },
result.stderr,
);
});
test("candidate binding: a linked worktree Git pointer is refused without replacing owner data", async () => {
const root = makeRepo();
const linkedRoot = join(tmpdir(), `gate-linked-${process.pid}-${Date.now()}`);
git(root, "worktree", "add", "-q", "-b", "linked", linkedRoot);
const verdictPath = join(linkedRoot, ".git");
const ownerData = readFileSync(verdictPath, "utf8");
const result = await runGate(linkedRoot, { OPENRIG_GATE_VERDICT: verdictPath });
assert.deepEqual(
{
refused: result.status !== 0,
ownerPreserved: existsSync(verdictPath) && readFileSync(verdictPath, "utf8") === ownerData,
},
{ refused: true, ownerPreserved: true },
result.stderr,
);
});
// The tracked-destination, tracked-symlink-leaf, tracked-symlink-parent, .git-metadata and
// linked-worktree-pointer fixtures were unbuilt with the legs they pinned (founder ruling,
// over-engineering audit — each required an adversary inside this trust domain). The plain
// outside-the-worktree typo guard below is the one that stayed.
test("candidate binding: a foreign verdict destination is refused without replacing owner data", async () => {
const root = makeRepo();
const foreignRoot = mkdtempSync(join(tmpdir(), "gate-foreign-owner-"));
+8 -53
View File
@@ -50,6 +50,10 @@ function readCandidateSha(repoRoot) {
return sha;
}
// Canonicalize the destination so containment compares like with like. This is NOT an adversary
// guard: repoRoot is realpath'd, and on macOS an ordinary tmpdir is `/var/...` symlinked to
// `/private/var/...`, so without this an entirely honest path reads as "outside the worktree".
// Resolves the existing prefix and re-appends the not-yet-created segments.
function canonicalizeDestination(destination) {
let cursor = resolve(destination);
const missingSegments = [];
@@ -77,66 +81,17 @@ function containedRelativePath(root, candidate) {
return rel.split(sep).join("/");
}
function isInsideOrEqual(root, candidate) {
const rel = relative(root, candidate);
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
}
function configuredPathRelativeToRoot(root, destination) {
let cursor = resolve(destination);
const segments = [];
let match = null;
while (true) {
try {
if (realpathSync(cursor) === root) match = segments.join("/");
} catch (error) {
if (error?.code !== "ENOENT" && error?.code !== "ENOTDIR") throw error;
}
const parent = dirname(cursor);
if (parent === cursor) return match;
segments.unshift(basename(cursor));
cursor = parent;
}
}
// Plain containment: a verdict written outside the worktree is a TYPO, and this catches it before the
// run mutates anything. The former tracked-path / tracked-symlink-leaf / .git-administrative legs were
// unbuilt (founder ruling, over-engineering audit): each needed an adversary inside this trust domain,
// and the only caller is the desk running its own gate.
function resolveVerdictDestination(repoRoot) {
const root = realpathSync(repoRoot);
const configuredPath = resolve(VERDICT_PATH);
const configuredRelativePath = configuredPathRelativeToRoot(root, configuredPath);
if (!configuredRelativePath) {
throw new Error(`verdict destination must be inside the current worktree: ${configuredPath}`);
}
if (configuredRelativePath === ".git" || configuredRelativePath.startsWith(".git/")) {
throw new Error(`verdict destination must not use Git administrative paths: ${configuredRelativePath}`);
}
const trackedEntries = new Set(runGit(["ls-files", "-z"], repoRoot).split("\0").filter(Boolean));
const configuredSegments = configuredRelativePath.split("/");
let configuredPrefix = "";
for (const segment of configuredSegments) {
configuredPrefix = configuredPrefix ? `${configuredPrefix}/${segment}` : segment;
if (trackedEntries.has(configuredPrefix)) {
throw new Error(`verdict destination must not traverse a tracked path: ${configuredPrefix}`);
}
}
const absolutePath = canonicalizeDestination(VERDICT_PATH);
const relativePath = containedRelativePath(root, absolutePath);
if (!relativePath) {
throw new Error(`verdict destination must be inside the current worktree: ${absolutePath}`);
}
const gitAdminRoots = runGit(["rev-parse", "--path-format=absolute", "--git-dir", "--git-common-dir"], repoRoot)
.trim()
.split("\n")
.filter(Boolean)
.map((path) => realpathSync(path));
if (gitAdminRoots.some((gitAdminRoot) => isInsideOrEqual(gitAdminRoot, absolutePath))) {
throw new Error(`verdict destination must not use Git administrative paths: ${absolutePath}`);
}
if (trackedEntries.has(relativePath)) {
throw new Error(`verdict destination must be untracked: ${relativePath}`);
}
return { absolutePath, relativePath };
}