Merge remote-tracking branch 'gitea/main'

# Conflicts:
#	prompts/owner-common-paired-room.md
#	src/channels/discord.ts
#	src/codex-warmup.ts
#	src/db/bootstrap.test.ts
#	src/db/migrations/index.ts
#	src/paired-execution-context-reviewer.ts
#	src/usage-primer.test.ts
#	src/usage-primer.ts
This commit is contained in:
Codex
2026-06-18 05:41:07 +09:00
22 changed files with 630 additions and 29 deletions

View File

@@ -6,8 +6,17 @@ import { afterEach, describe, expect, it } from 'vitest';
import { applyBaseSchema } from './base-schema.js';
import { initializeDatabaseSchema } from './bootstrap.js';
import { applyVersionedSchemaMigrations } from './migrations/index.js';
import { applyLegacySchemaMigrations } from './schema.js';
function tableColumns(database: Database, table: string): string[] {
return (
database.prepare(`PRAGMA table_info(${table})`).all() as Array<{
name: string;
}>
).map((column) => column.name);
}
function getAppliedSchemaMigrations(
database: Database,
): Array<{ version: number; name: string }> {
@@ -45,6 +54,8 @@ function getExpectedSchemaMigrations(): Array<{
{ version: 18, name: 'paired_turn_output_attachments' },
{ version: 19, name: 'turn_progress_text_recovery' },
{ version: 20, name: 'arbiter_intervention_count' },
{ version: 21, name: 'reviewer_failure_count' },
{ version: 22, name: 'turn_progress_text_compat' },
];
}
@@ -115,4 +126,55 @@ describe('initializeDatabaseSchema', () => {
reopened.close();
}
});
it('backfills turn_progress_text columns when version 15 was recorded under a different name', () => {
const database = new Database(':memory:');
try {
applyBaseSchema(database);
// Reproduce a deployment that first shipped reviewer_failure_count as a
// local migration numbered 15, before turn_progress_text claimed that
// version upstream. The runner skips by version number, so migration 015
// (turn_progress_text) would otherwise never run on this database.
database.exec(`
CREATE TABLE schema_migrations (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`);
const collidedNames: Record<number, string> = {
15: 'reviewer_failure_count',
};
for (let version = 1; version <= 15; version += 1) {
database
.prepare('INSERT INTO schema_migrations (version, name) VALUES (?, ?)')
.run(version, collidedNames[version] ?? `legacy_${version}`);
}
// Precondition: the collided database is missing the progress columns.
expect(tableColumns(database, 'paired_turns')).not.toContain(
'progress_text',
);
applyVersionedSchemaMigrations(database, { assistantName: 'Andy' });
const columns = tableColumns(database, 'paired_turns');
expect(columns).toContain('progress_text');
expect(columns).toContain('progress_updated_at');
// The pre-existing version-15 row is left untouched; the compat migration
// (version 20) is what reconciles the schema.
const version15 = database
.prepare('SELECT name FROM schema_migrations WHERE version = 15')
.get() as { name: string };
expect(version15.name).toBe('reviewer_failure_count');
const version20 = database
.prepare('SELECT name FROM schema_migrations WHERE version = 20')
.get() as { name: string } | null;
expect(version20?.name).toBe('turn_progress_text_compat');
} finally {
database.close();
}
});
});

View File

@@ -0,0 +1,23 @@
import type { Database } from 'bun:sqlite';
import { tableHasColumn } from './helpers.js';
import type { SchemaMigrationDefinition } from './types.js';
export const REVIEWER_FAILURE_COUNT_MIGRATION: SchemaMigrationDefinition = {
version: 21,
name: 'reviewer_failure_count',
apply(database: Database) {
if (!tableHasColumn(database, 'paired_tasks', 'reviewer_failure_count')) {
database.exec(`
ALTER TABLE paired_tasks
ADD COLUMN reviewer_failure_count INTEGER NOT NULL DEFAULT 0
`);
}
database.exec(`
UPDATE paired_tasks
SET reviewer_failure_count = 0
WHERE reviewer_failure_count IS NULL
`);
},
};

View File

@@ -0,0 +1,32 @@
import type { Database } from 'bun:sqlite';
import { tableHasColumn } from './helpers.js';
import type { SchemaMigrationDefinition } from './types.js';
/**
* Compatibility backfill for `turn_progress_text` (migration 015).
*
* Some deployments first shipped `reviewer_failure_count` as a local migration
* numbered 15, so their `schema_migrations` table records version 15 under a
* different name than the canonical `turn_progress_text` migration. The runner
* skips migrations purely by version number, so on those databases the real
* version-15 migration never runs and `paired_turns.progress_text` /
* `progress_updated_at` are missing — yet the runtime reads them. This migration
* re-adds the columns idempotently so collided databases converge with fresh
* ones. On a fresh database migration 015 already created the columns, so the
* `tableHasColumn` guards make this a no-op.
*/
export const TURN_PROGRESS_TEXT_COMPAT_MIGRATION: SchemaMigrationDefinition = {
version: 22,
name: 'turn_progress_text_compat',
apply(database: Database) {
if (!tableHasColumn(database, 'paired_turns', 'progress_text')) {
database.exec(`ALTER TABLE paired_turns ADD COLUMN progress_text TEXT`);
}
if (!tableHasColumn(database, 'paired_turns', 'progress_updated_at')) {
database.exec(
`ALTER TABLE paired_turns ADD COLUMN progress_updated_at TEXT`,
);
}
},
};

View File

@@ -20,6 +20,8 @@ import { SCHEDULED_TASK_ROOM_ROLE_MIGRATION } from './017_scheduled-task-room-ro
import { PAIRED_TURN_OUTPUT_ATTACHMENTS_MIGRATION } from './018_paired-turn-output-attachments.js';
import { TURN_PROGRESS_TEXT_RECOVERY_MIGRATION } from './019_turn-progress-text-recovery.js';
import { ARBITER_INTERVENTION_COUNT_MIGRATION } from './020_arbiter-intervention-count.js';
import { REVIEWER_FAILURE_COUNT_MIGRATION } from './021_reviewer-failure-count.js';
import { TURN_PROGRESS_TEXT_COMPAT_MIGRATION } from './022_turn-progress-text-compat.js';
import type {
SchemaMigrationArgs,
SchemaMigrationDefinition,
@@ -48,6 +50,8 @@ const ORDERED_SCHEMA_MIGRATIONS: readonly SchemaMigrationDefinition[] = [
PAIRED_TURN_OUTPUT_ATTACHMENTS_MIGRATION,
TURN_PROGRESS_TEXT_RECOVERY_MIGRATION,
ARBITER_INTERVENTION_COUNT_MIGRATION,
REVIEWER_FAILURE_COUNT_MIGRATION,
TURN_PROGRESS_TEXT_COMPAT_MIGRATION,
];
function ensureSchemaMigrationsTable(database: Database): void {

View File

@@ -50,6 +50,7 @@ export type PairedTaskUpdates = Partial<
| 'review_requested_at'
| 'round_trip_count'
| 'owner_failure_count'
| 'reviewer_failure_count'
| 'owner_step_done_streak'
| 'finalize_step_done_count'
| 'task_done_then_user_reopen_count'
@@ -177,6 +178,7 @@ export function createPairedTaskInDatabase(
review_requested_at,
round_trip_count,
owner_failure_count,
reviewer_failure_count,
owner_step_done_streak,
finalize_step_done_count,
task_done_then_user_reopen_count,
@@ -207,6 +209,7 @@ export function createPairedTaskInDatabase(
task.review_requested_at,
task.round_trip_count,
task.owner_failure_count ?? 0,
task.reviewer_failure_count ?? 0,
task.owner_step_done_streak ?? 0,
task.finalize_step_done_count ?? 0,
task.task_done_then_user_reopen_count ?? 0,
@@ -342,6 +345,10 @@ export function updatePairedTaskInDatabase(
fields.push('owner_failure_count = ?');
values.push(updates.owner_failure_count);
}
if (updates.reviewer_failure_count !== undefined) {
fields.push('reviewer_failure_count = ?');
values.push(updates.reviewer_failure_count);
}
if (updates.owner_step_done_streak !== undefined) {
fields.push('owner_step_done_streak = ?');
values.push(updates.owner_step_done_streak);
@@ -425,6 +432,10 @@ export function updatePairedTaskIfUnchangedInDatabase(
fields.push('owner_failure_count = ?');
values.push(updates.owner_failure_count);
}
if (updates.reviewer_failure_count !== undefined) {
fields.push('reviewer_failure_count = ?');
values.push(updates.reviewer_failure_count);
}
if (updates.owner_step_done_streak !== undefined) {
fields.push('owner_step_done_streak = ?');
values.push(updates.owner_step_done_streak);