headlesscode 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,42 @@
1
1
  import * as fs from "node:fs/promises"
2
2
  import * as path from "node:path"
3
+ import { readArchive, writeArchive } from "./archive.js"
3
4
  import type { CandidateRecord, CurriculumTask, RsiArchive } from "./types.js"
5
+ import completionDiscipline from "../../fixtures/rsi-curriculum/completion-discipline.json"
6
+ import regressionRecovery from "../../fixtures/rsi-curriculum/regression-recovery.json"
7
+ import toolEfficiency from "../../fixtures/rsi-curriculum/tool-efficiency.json"
8
+ import generalization from "../../fixtures/rsi-curriculum/generalization.json"
4
9
 
5
- const FAILURE_TASKS: Record<string, { capability: string; difficulty: CurriculumTask["difficulty"]; template: string }> = {
10
+ interface CurriculumFixture {
11
+ id: string
12
+ description: string
13
+ command: string
14
+ capability: string
15
+ task: string
16
+ }
17
+
18
+ const FIXTURE_MANIFESTS = { "completion-discipline": completionDiscipline, "regression-recovery": regressionRecovery, "tool-efficiency": toolEfficiency, generalization }
19
+ const FIXTURES: Record<string, CurriculumFixture> = Object.fromEntries(Object.entries(FIXTURE_MANIFESTS).map(([id, fixture]) => {
20
+ if (fixture.id !== id || typeof fixture.task !== "string" || !fixture.task || typeof fixture.capability !== "string" || !fixture.capability || typeof fixture.description !== "string" || !fixture.description) throw new Error(`curriculum fixture manifest ${id} is missing registered task metadata`)
21
+ return [id, { id, description: fixture.description, command: `node fixtures/rsi-curriculum/validate.mjs --fixture ${id}`, capability: fixture.capability, task: fixture.task }]
22
+ }))
23
+
24
+ const FAILURE_TASKS: Record<string, { difficulty: CurriculumTask["difficulty"]; fixtureId: string }> = {
6
25
  "iteration-cap": {
7
- capability: "completion-discipline",
8
26
  difficulty: 3,
9
- template: "Complete a bounded implementation task and prove it with a final verification command before the iteration budget expires.",
27
+ fixtureId: "completion-discipline",
10
28
  },
11
29
  "regression-failure": {
12
- capability: "regression-recovery",
13
30
  difficulty: 3,
14
- template: "Repair a deliberately failing change while preserving the existing regression suite and report the verified result.",
31
+ fixtureId: "regression-recovery",
15
32
  },
16
33
  "timeout": {
17
- capability: "tool-efficiency",
18
34
  difficulty: 4,
19
- template: "Solve a repository task under a strict command-time budget without repeating unproductive exploration.",
35
+ fixtureId: "tool-efficiency",
20
36
  },
21
37
  "evaluation-failure": {
22
- capability: "generalization",
23
38
  difficulty: 4,
24
- template: "Make a focused repository improvement that passes the visible suite and an adjacent executable behavior check.",
39
+ fixtureId: "generalization",
25
40
  },
26
41
  }
27
42
 
@@ -43,26 +58,130 @@ export function generateCurriculumProposals(archive: RsiArchive, now = new Date(
43
58
  }
44
59
  return [...grouped.entries()].flatMap(([failureClass, candidates]) => {
45
60
  const definition = FAILURE_TASKS[failureClass] ?? FAILURE_TASKS["evaluation-failure"]
61
+ const fixture = FIXTURES[definition.fixtureId]!
62
+ const existing = archive.curriculumTasks.find((task) => task.id === `curriculum-${failureClass}` && task.fixtureId === fixture.id && task.task === fixture.task)
46
63
  return [{
47
64
  id: `curriculum-${failureClass}`,
48
- task: definition.template,
65
+ task: fixture.task,
49
66
  difficulty: definition.difficulty,
50
- capability: definition.capability,
51
- groundTruthCommand: "npm test",
67
+ capability: fixture.capability,
68
+ groundTruthCommand: fixture.command,
69
+ fixtureId: fixture.id,
70
+ fixtureDescription: fixture.description,
52
71
  provenance: { sourceCandidateIds: candidates.map((candidate) => candidate.id), failureClass, generatedAt: now },
53
- validated: false,
72
+ validated: existing?.validated ?? false,
73
+ ...(existing?.validation ? { validation: existing.validation } : {}),
74
+ ...(existing?.promotedAt ? { promotedAt: existing.promotedAt } : {}),
54
75
  }]
55
76
  })
56
77
  }
57
78
 
58
79
  export function validateCurriculumTask(task: CurriculumTask): boolean {
59
- return task.task.trim() !== "" && task.groundTruthCommand.trim() !== "" && task.provenance.sourceCandidateIds.length > 0
80
+ if (!task || typeof task !== "object") return false
81
+ if (typeof task.fixtureId !== "string" || typeof task.task !== "string" || typeof task.capability !== "string" || typeof task.groundTruthCommand !== "string" || typeof task.fixtureDescription !== "string") return false
82
+ const provenance = task.provenance as unknown
83
+ if (!provenance || typeof provenance !== "object") return false
84
+ const candidateIds = (provenance as { sourceCandidateIds?: unknown }).sourceCandidateIds
85
+ if (!Array.isArray(candidateIds) || candidateIds.length === 0 || candidateIds.some((id) => typeof id !== "string" || id.length === 0)) return false
86
+ const fixture = FIXTURES[task.fixtureId]
87
+ return Boolean(fixture && task.task === fixture.task && task.capability === fixture.capability && task.groundTruthCommand === fixture.command && task.fixtureDescription === fixture.description)
88
+ }
89
+
90
+ export interface CurriculumReplayResult {
91
+ ok: boolean
92
+ exitCode: number | null
93
+ outputSha256?: string
94
+ failure?: string
95
+ }
96
+
97
+ export async function validateCurriculumProposals(
98
+ tasks: CurriculumTask[],
99
+ baseCommit: string,
100
+ runReplay: (task: CurriculumTask, attempt: 1 | 2, idempotencyKey: string) => Promise<CurriculumReplayResult>,
101
+ now = new Date().toISOString(),
102
+ ): Promise<CurriculumTask[]> {
103
+ return Promise.all(tasks.map(async (task) => {
104
+ const replays: NonNullable<CurriculumTask["validation"]>["replays"] = []
105
+ let reason = ""
106
+ if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/i.test(baseCommit)) reason = "clean base commit is missing or malformed"
107
+ else if (!validateCurriculumTask(task)) reason = "proposal does not resolve to an exact registered fixture and command"
108
+ if (!reason) {
109
+ for (const attempt of [1, 2] as const) {
110
+ const idempotencyKey = `curriculum:${task.id}:replay:${attempt}`
111
+ try {
112
+ const result = await runReplay(task, attempt, idempotencyKey)
113
+ replays.push({
114
+ idempotencyKey,
115
+ ok: result.ok,
116
+ exitCode: result.exitCode,
117
+ ...(result.outputSha256 ? { outputSha256: result.outputSha256 } : {}),
118
+ ...(result.failure ? { failure: result.failure.slice(0, 1000) } : {}),
119
+ })
120
+ if (!result.ok || result.exitCode !== 0 || !/^[a-f0-9]{64}$/i.test(result.outputSha256 ?? "")) {
121
+ reason = result.failure ?? `fixture replay ${attempt} did not produce a successful output artifact`
122
+ break
123
+ }
124
+ } catch (error) {
125
+ reason = error instanceof Error ? error.message.slice(0, 1000) : String(error).slice(0, 1000)
126
+ break
127
+ }
128
+ }
129
+ }
130
+ const first = replays[0]
131
+ const second = replays[1]
132
+ const reproducible = Boolean(!reason && first?.ok && second?.ok && first.exitCode === second.exitCode && first.outputSha256 === second.outputSha256)
133
+ if (!reason && !reproducible) reason = "two successful clean replays did not produce the same output digest"
134
+ return {
135
+ ...task,
136
+ validated: reproducible,
137
+ validation: { baseCommit, command: task.groundTruthCommand, validatedAt: now, replays, reproducible, reason: reason || "two clean OpenShell replays passed reference tests, rejected a known mutant, and had identical output digests" },
138
+ }
139
+ }))
140
+ }
141
+
142
+ export function promoteValidatedCurriculumTask(task: CurriculumTask, promotedAt = new Date().toISOString()): CurriculumTask {
143
+ const validation = task.validation
144
+ const replays = Array.isArray(validation?.replays) ? validation.replays : []
145
+ const digest = replays[0]?.outputSha256
146
+ const evidenceValid = validateCurriculumTask(task)
147
+ && task.validated === true
148
+ && validation?.reproducible === true
149
+ && /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/i.test(validation.baseCommit)
150
+ && validation.command === task.groundTruthCommand
151
+ && replays.length === 2
152
+ && replays.every((replay, index) => replay.ok === true && replay.exitCode === 0 && replay.idempotencyKey === `curriculum:${task.id}:replay:${index + 1}` && /^[a-f0-9]{64}$/i.test(replay.outputSha256 ?? "") && replay.outputSha256 === digest)
153
+ if (!evidenceValid) throw new Error(`curriculum task ${task.id} cannot be promoted without valid reproducible fixture evidence`)
154
+ return { ...task, promotedAt }
155
+ }
156
+
157
+ export async function promoteArchivedCurriculumTask(archiveDir: string, taskId: string, promotedAt = new Date().toISOString()): Promise<CurriculumTask> {
158
+ const archive = await readArchive(archiveDir)
159
+ const task = archive.curriculumTasks.find((entry) => entry.id === taskId)
160
+ if (!task) throw new Error(`curriculum task ${taskId} was not found in the archive`)
161
+ const promoted = promoteValidatedCurriculumTask(task, promotedAt)
162
+ archive.curriculumTasks = archive.curriculumTasks.map((entry) => entry.id === taskId ? promoted : entry)
163
+ for (const run of [...archive.runs, ...archive.activeRuns]) run.curriculumTasks = (run.curriculumTasks ?? []).map((entry) => entry.id === taskId ? promoted : entry)
164
+ const reportPaths = [...new Set([...archive.runs, ...archive.activeRuns].flatMap((run) => run.curriculumTasks?.some((entry) => entry.id === taskId) ? run.reports : []))]
165
+ const backups = new Map<string, string>()
166
+ for (const reportPath of reportPaths) backups.set(reportPath, await fs.readFile(reportPath, "utf8"))
167
+ try {
168
+ for (const reportPath of reportPaths) await fs.writeFile(reportPath, `${backups.get(reportPath)}\nCurriculum task ${taskId} was explicitly promoted at ${promotedAt}.\n`, "utf8")
169
+ await writeArchive(archiveDir, archive)
170
+ } catch (error) {
171
+ const rollbackFailures: string[] = []
172
+ for (const [reportPath, content] of backups) {
173
+ try { await fs.writeFile(reportPath, content, "utf8") }
174
+ catch (rollbackError) { rollbackFailures.push(`${reportPath}: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`) }
175
+ }
176
+ if (rollbackFailures.length) throw new Error(`promotion failed: ${error instanceof Error ? error.message : String(error)}; report rollback failed: ${rollbackFailures.join("; ")}`)
177
+ throw error
178
+ }
179
+ return promoted
60
180
  }
61
181
 
62
182
  export async function writeCurriculumProposals(tasks: CurriculumTask[], outputDir: string): Promise<string> {
63
183
  await fs.mkdir(outputDir, { recursive: true })
64
- const validated = tasks.map((task) => ({ ...task, validated: validateCurriculumTask(task) }))
65
184
  const outputPath = path.join(outputDir, "proposals.json")
66
- await fs.writeFile(outputPath, `${JSON.stringify(validated, null, 2)}\n`, "utf8")
185
+ await fs.writeFile(outputPath, `${JSON.stringify(tasks, null, 2)}\n`, "utf8")
67
186
  return outputPath
68
187
  }
@@ -1,36 +1,16 @@
1
- import { exec as execCallback } from "node:child_process"
2
- import { promisify } from "node:util"
3
1
  import type { CommandRunner, EvaluationSummary, RsiConfig, TrialResult } from "./types.js"
4
2
  import { changedFiles, candidateCommits, gitOutput } from "./workspace.js"
5
3
  import { protectedPathViolations } from "./sandbox.js"
6
-
7
- const exec = promisify(execCallback)
8
-
9
- export const runCommand: CommandRunner = async (command, cwd, timeoutMs, env) => {
10
- const started = Date.now()
11
- try {
12
- const result = await exec(command, { cwd, env, timeout: timeoutMs, maxBuffer: 8 * 1024 * 1024 })
13
- return { ok: true, command, exitCode: 0, durationMs: Date.now() - started, stdout: result.stdout, stderr: result.stderr }
14
- } catch (error) {
15
- const failure = error as { code?: number | string; killed?: boolean; stdout?: string; stderr?: string }
16
- return {
17
- ok: false,
18
- command,
19
- exitCode: typeof failure.code === "number" ? failure.code : null,
20
- durationMs: Date.now() - started,
21
- stdout: failure.stdout ?? "",
22
- stderr: failure.stderr ?? String(error),
23
- timedOut: failure.killed === true,
24
- }
25
- }
26
- }
4
+ import { createOpenShellCommandRunner } from "./openshell.js"
27
5
 
28
6
  export async function evaluateCandidate(
29
7
  config: RsiConfig,
30
8
  candidateRoot: string,
31
9
  baseCommit: string,
32
- runner: CommandRunner = runCommand,
10
+ runner?: CommandRunner,
33
11
  ): Promise<EvaluationSummary> {
12
+ if (config.hiddenEvalCommands.length > 0) throw new Error("RSI hidden evaluations require a supervisor-only evaluator service; refusing to run hidden commands in a candidate guest")
13
+ const isolatedRunner = runner ?? createOpenShellCommandRunner(config)
34
14
  const files = await changedFiles(candidateRoot, baseCommit)
35
15
  const commits = await candidateCommits(candidateRoot, baseCommit)
36
16
  const complexity = await complexityMetrics(candidateRoot, baseCommit, files)
@@ -59,23 +39,12 @@ export async function evaluateCandidate(
59
39
  failureClassification: "protected-path-violation",
60
40
  }
61
41
  }
62
- const regression = await runner("npm test", candidateRoot, config.commandTimeoutMs)
42
+ const regression = await isolatedRunner(config.regressionCommand, candidateRoot, config.commandTimeoutMs)
63
43
  const visible: TrialResult[] = []
64
44
  for (const command of config.evalCommands) {
65
- visible.push(await runner(command, candidateRoot, config.commandTimeoutMs))
45
+ visible.push(await isolatedRunner(command, candidateRoot, config.commandTimeoutMs))
66
46
  }
67
47
  const hidden: TrialResult[] = []
68
- for (const command of config.hiddenEvalCommands) {
69
- // Hidden commands belong to the supervisor checkout. They receive the
70
- // candidate path explicitly so a candidate cannot replace the evaluator
71
- // script or package metadata in the process that scores it.
72
- hidden.push(
73
- await runner(command, config.repoRoot, config.commandTimeoutMs, {
74
- ...process.env,
75
- HEADLESSCODE_RSI_CANDIDATE_ROOT: candidateRoot,
76
- }),
77
- )
78
- }
79
48
  return {
80
49
  regression,
81
50
  visible,
@@ -1,14 +1,16 @@
1
- import type { EvaluationSummary, Fitness, HardGates } from "./types.js"
1
+ import type { EvaluationSummary, Fitness, HardGates, PairedComparison } from "./types.js"
2
2
 
3
3
  function rate(passed: number, total: number): number {
4
4
  return total === 0 ? 1 : passed / total
5
5
  }
6
6
 
7
7
  export function hardGatesFor(summary: EvaluationSummary): HardGates {
8
+ const hasTaskEvaluation = summary.visible.length > 0 || summary.hidden.length > 0
8
9
  return {
9
10
  regressionPass: summary.regression.ok,
10
- visibleEvalPass: summary.visible.every((trial) => trial.ok),
11
+ visibleEvalPass: hasTaskEvaluation && summary.visible.every((trial) => trial.ok),
11
12
  hiddenEvalPass: summary.hidden.every((trial) => trial.ok),
13
+ adversarialPass: summary.adversarialPass !== false,
12
14
  noProtectedPathViolation: !summary.protectedPathViolation,
13
15
  completed: summary.completed,
14
16
  noCrash: !summary.crashed,
@@ -45,9 +47,11 @@ export function computeFitness(summary: EvaluationSummary): Fitness {
45
47
  complexityPenalty,
46
48
  }
47
49
  const components = { regression, visible: visibleRate, hidden: hiddenRate, efficiency, recovery }
48
- const score = Math.round(
50
+ const unpenalizedScore = Math.round(
49
51
  (regression * 45 + visibleRate * 25 + hiddenRate * 20 + efficiency * 5 + recovery * 5 - complexityPenalty * 5) * 100,
50
52
  ) / 100
53
+ const adversarialPenalty = Math.min(10, Math.max(0, summary.adversarialPenalty ?? 0))
54
+ const score = Math.max(0, Math.round((unpenalizedScore - adversarialPenalty) * 100) / 100)
51
55
  const failedGate = Object.entries(hardGates).find(([, passed]) => !passed)?.[0]
52
56
  return {
53
57
  score: failedGate ? 0 : score,
@@ -55,10 +59,41 @@ export function computeFitness(summary: EvaluationSummary): Fitness {
55
59
  metrics,
56
60
  complexity,
57
61
  hardGates,
58
- reason: failedGate ? `hard gate failed: ${failedGate}` : "all hard gates passed",
62
+ reason: failedGate ? `hard gate failed: ${failedGate}` : adversarialPenalty > 0 ? `all hard gates passed; adversarial findings penalty ${adversarialPenalty}` : "all hard gates passed",
63
+ ...(adversarialPenalty > 0 ? { adversarialPenalty } : {}),
59
64
  }
60
65
  }
61
66
 
62
67
  export function compareFitness(left: Fitness | undefined, right: Fitness | undefined): number {
63
68
  return (left?.score ?? 0) - (right?.score ?? 0)
64
69
  }
70
+
71
+ export function comparePairedEvaluation(baseline: EvaluationSummary, candidate: EvaluationSummary): PairedComparison {
72
+ const baselineTrials = [baseline.regression, ...baseline.visible, ...baseline.hidden]
73
+ const candidateTrials = [candidate.regression, ...candidate.visible, ...candidate.hidden]
74
+ const baselinePasses = baselineTrials.filter((trial) => trial.ok).length
75
+ const candidatePasses = candidateTrials.filter((trial) => trial.ok).length
76
+ const baselineDurationMs = baselineTrials.reduce((total, trial) => total + trial.durationMs, 0)
77
+ const candidateDurationMs = candidateTrials.reduce((total, trial) => total + trial.durationMs, 0)
78
+ const durationRatio = baselineDurationMs === 0 ? Number.POSITIVE_INFINITY : candidateDurationMs / baselineDurationMs
79
+ const comparable = baselineTrials.length === candidateTrials.length && baselineTrials.every((trial, index) => trial.command === candidateTrials[index]?.command)
80
+ const correctnessImproved = comparable && candidatePasses > baselinePasses
81
+ const efficiencyImproved = comparable && candidatePasses === baselinePasses && candidatePasses === candidateTrials.length && durationRatio <= 0.9
82
+ const improved = correctnessImproved || efficiencyImproved
83
+ return {
84
+ baselinePasses,
85
+ candidatePasses,
86
+ trialCount: Math.min(baselineTrials.length, candidateTrials.length),
87
+ baselineDurationMs,
88
+ candidateDurationMs,
89
+ durationRatio,
90
+ improved,
91
+ reason: !comparable
92
+ ? "baseline and candidate did not run the same evaluation commands"
93
+ : correctnessImproved
94
+ ? "candidate passed more paired evaluation trials"
95
+ : efficiencyImproved
96
+ ? "candidate passed every paired trial and reduced total evaluation duration by at least 10%"
97
+ : "candidate did not meet the paired improvement threshold",
98
+ }
99
+ }
package/src/rsi/index.ts CHANGED
@@ -13,4 +13,5 @@ export * from "./workspace.js"
13
13
  export * from "./trajectory.js"
14
14
  export * from "./curriculum.js"
15
15
  export * from "./roles.js"
16
+ export * from "./adversarial.js"
16
17
  export * from "./search.js"
@@ -0,0 +1,65 @@
1
+ CREATE TABLE IF NOT EXISTS headlesscode_rsi_queue_policy (
2
+ queue_name text PRIMARY KEY,
3
+ max_in_flight integer NOT NULL CHECK (max_in_flight > 0),
4
+ lease_ms integer NOT NULL CHECK (lease_ms >= 1000),
5
+ max_attempts integer NOT NULL CHECK (max_attempts > 0),
6
+ class_capacity jsonb NOT NULL DEFAULT '{}'::jsonb,
7
+ key_capacity jsonb NOT NULL DEFAULT '{}'::jsonb,
8
+ updated_at timestamptz NOT NULL DEFAULT clock_timestamp()
9
+ );
10
+
11
+ CREATE TABLE IF NOT EXISTS headlesscode_rsi_workers (
12
+ queue_name text NOT NULL REFERENCES headlesscode_rsi_queue_policy(queue_name),
13
+ worker_id text NOT NULL,
14
+ gateway_id text NOT NULL,
15
+ hostname text NOT NULL,
16
+ max_active integer NOT NULL CHECK (max_active > 0),
17
+ resource_classes jsonb NOT NULL DEFAULT '[]'::jsonb,
18
+ active boolean NOT NULL DEFAULT true,
19
+ registered_at timestamptz NOT NULL DEFAULT clock_timestamp(),
20
+ heartbeat_at timestamptz NOT NULL DEFAULT clock_timestamp(),
21
+ PRIMARY KEY(queue_name, worker_id)
22
+ );
23
+
24
+ CREATE TABLE IF NOT EXISTS headlesscode_rsi_artifacts (
25
+ sha256 char(64) PRIMARY KEY,
26
+ storage_id text NOT NULL,
27
+ object_key text NOT NULL,
28
+ backend text NOT NULL CHECK (backend IN ('s3', 'file')),
29
+ byte_length bigint NOT NULL CHECK (byte_length > 0),
30
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp()
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS headlesscode_rsi_jobs (
34
+ job_id uuid PRIMARY KEY,
35
+ queue_name text NOT NULL REFERENCES headlesscode_rsi_queue_policy(queue_name),
36
+ idempotency_key text NOT NULL,
37
+ run_id text NOT NULL,
38
+ candidate_id text NOT NULL,
39
+ job_kind text NOT NULL CHECK (job_kind IN ('mutation', 'evaluation')),
40
+ resource_class text NOT NULL CHECK (resource_class IN ('LOCAL_GPU', 'CPU', 'REMOTE_API', 'TRAINING_GPU')),
41
+ concurrency_key text,
42
+ artifact_sha256 char(64) NOT NULL REFERENCES headlesscode_rsi_artifacts(sha256),
43
+ payload jsonb NOT NULL,
44
+ status text NOT NULL CHECK (status IN ('queued', 'leased', 'completed', 'failed', 'cancelled')),
45
+ attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
46
+ max_attempts integer NOT NULL CHECK (max_attempts > 0),
47
+ available_at timestamptz NOT NULL DEFAULT clock_timestamp(),
48
+ lease_owner text,
49
+ lease_token uuid,
50
+ lease_expires_at timestamptz,
51
+ result jsonb,
52
+ error text,
53
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
54
+ updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
55
+ completed_at timestamptz,
56
+ CHECK ((status = 'leased') = (lease_owner IS NOT NULL AND lease_token IS NOT NULL AND lease_expires_at IS NOT NULL)),
57
+ UNIQUE(queue_name, idempotency_key)
58
+ );
59
+
60
+ CREATE INDEX IF NOT EXISTS headlesscode_rsi_jobs_claim_idx
61
+ ON headlesscode_rsi_jobs (queue_name, status, available_at, created_at)
62
+ WHERE status IN ('queued', 'leased');
63
+ CREATE INDEX IF NOT EXISTS headlesscode_rsi_jobs_lease_idx
64
+ ON headlesscode_rsi_jobs (lease_expires_at)
65
+ WHERE status = 'leased';
@@ -0,0 +1,39 @@
1
+ DO $$
2
+ DECLARE legacy_column record;
3
+ DECLARE has_legacy_bytes boolean;
4
+ BEGIN
5
+ FOR legacy_column IN
6
+ SELECT column_name FROM information_schema.columns
7
+ WHERE table_schema=current_schema() AND table_name='headlesscode_rsi_artifacts' AND data_type='bytea'
8
+ LOOP
9
+ EXECUTE format('SELECT EXISTS (SELECT 1 FROM headlesscode_rsi_artifacts WHERE %I IS NOT NULL)', legacy_column.column_name) INTO has_legacy_bytes;
10
+ IF has_legacy_bytes THEN
11
+ RAISE EXCEPTION 'RSI artifact column % still contains PostgreSQL blobs; upload legacy bytes to the configured shared object store and migrate their metadata first', legacy_column.column_name;
12
+ END IF;
13
+ EXECUTE format('ALTER TABLE headlesscode_rsi_artifacts DROP COLUMN %I', legacy_column.column_name);
14
+ END LOOP;
15
+ END $$;
16
+
17
+ ALTER TABLE headlesscode_rsi_artifacts ADD COLUMN IF NOT EXISTS storage_id text;
18
+ ALTER TABLE headlesscode_rsi_artifacts ADD COLUMN IF NOT EXISTS object_key text;
19
+ ALTER TABLE headlesscode_rsi_artifacts ADD COLUMN IF NOT EXISTS backend text;
20
+ ALTER TABLE headlesscode_rsi_artifacts ADD COLUMN IF NOT EXISTS byte_length bigint;
21
+
22
+ DO $$
23
+ BEGIN
24
+ IF EXISTS (
25
+ SELECT 1 FROM headlesscode_rsi_artifacts
26
+ WHERE storage_id IS NULL OR object_key IS NULL OR backend IS NULL OR byte_length IS NULL
27
+ ) THEN
28
+ RAISE EXCEPTION 'RSI artifacts need object-store references before migration 002; upload legacy artifact bytes to the configured shared object store and migrate their metadata first';
29
+ END IF;
30
+ END $$;
31
+
32
+ ALTER TABLE headlesscode_rsi_artifacts ALTER COLUMN storage_id SET NOT NULL;
33
+ ALTER TABLE headlesscode_rsi_artifacts ALTER COLUMN object_key SET NOT NULL;
34
+ ALTER TABLE headlesscode_rsi_artifacts ALTER COLUMN backend SET NOT NULL;
35
+ ALTER TABLE headlesscode_rsi_artifacts ALTER COLUMN byte_length SET NOT NULL;
36
+ CREATE UNIQUE INDEX IF NOT EXISTS headlesscode_rsi_artifacts_object_key_idx ON headlesscode_rsi_artifacts(object_key);
37
+
38
+ ALTER TABLE headlesscode_rsi_jobs
39
+ ADD COLUMN IF NOT EXISTS lease_duration_ms integer NOT NULL DEFAULT 3600000 CHECK (lease_duration_ms >= 1000);
@@ -0,0 +1,6 @@
1
+ ALTER TABLE headlesscode_rsi_jobs
2
+ DROP CONSTRAINT IF EXISTS headlesscode_rsi_jobs_job_kind_check;
3
+
4
+ ALTER TABLE headlesscode_rsi_jobs
5
+ ADD CONSTRAINT headlesscode_rsi_jobs_job_kind_check
6
+ CHECK (job_kind IN ('mutation', 'evaluation', 'training', 'model-evaluation'));