Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export const ProjectConfigSchema = z.object({
squintDbUrl: z.string().url().optional(),
runLinksEnabled: z.boolean().default(false),
maxInFlightItems: z.number().int().positive().optional(),
snapshotEnabled: z.boolean().optional(),
snapshotTtlMs: z.number().int().positive().optional(),
});

export const CascadeConfigSchema = z.object({
Expand Down
7 changes: 7 additions & 0 deletions src/db/migrations/0046_add_snapshot_policy.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Add per-project snapshot policy columns to projects table.
-- NULL means fall back to router-level defaults.
-- snapshot_enabled: when NULL, router default (false) applies.
-- snapshot_ttl_ms: when NULL, router default applies.

ALTER TABLE projects ADD COLUMN snapshot_enabled BOOLEAN DEFAULT NULL;
ALTER TABLE projects ADD COLUMN snapshot_ttl_ms INTEGER DEFAULT NULL;
7 changes: 7 additions & 0 deletions src/db/migrations/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,13 @@
"when": 1780000000000,
"tag": "0045_agent_config_prompts",
"breakpoints": false
},
{
"idx": 46,
"version": "7",
"when": 1781000000000,
"tag": "0046_add_snapshot_policy",
"breakpoints": false
}
]
}
46 changes: 29 additions & 17 deletions src/db/repositories/configMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export interface ProjectConfigRaw {
agentEngineSettings?: Record<string, EngineSettings>;
runLinksEnabled?: boolean;
maxInFlightItems?: number;
snapshotEnabled?: boolean;
snapshotTtlMs?: number;
trello?: {
boardId: string;
lists: Record<string, string>;
Expand Down Expand Up @@ -130,6 +132,8 @@ type ProjectRow = {
agentEngineSettings: EngineSettings | null;
runLinksEnabled: boolean;
maxInFlightItems: number | null;
snapshotEnabled: boolean | null;
snapshotTtlMs: number | null;
};

export function buildAgentMaps(configs: AgentConfigRow[]): {
Expand Down Expand Up @@ -190,6 +194,30 @@ function buildAgentEngineConfig(
};
}

function buildBaseProjectFields(row: ProjectRow, pmType: 'trello' | 'jira'): ProjectConfigRaw {
return {
id: row.id,
orgId: row.orgId,
name: row.name,
repo: row.repo ?? undefined,
baseBranch: row.baseBranch ?? 'main',
branchPrefix: row.branchPrefix ?? 'feature/',
pm: { type: pmType },
model: row.model ?? undefined,
maxIterations: row.maxIterations ?? undefined,
watchdogTimeoutMs: row.watchdogTimeoutMs ?? undefined,
progressModel: row.progressModel ?? undefined,
progressIntervalMinutes: numericOrUndefined(row.progressIntervalMinutes),
workItemBudgetUsd: numericOrUndefined(row.workItemBudgetUsd),
engineSettings: row.agentEngineSettings ?? undefined,
squintDbUrl: row.squintDbUrl ?? undefined,
runLinksEnabled: row.runLinksEnabled ?? false,
maxInFlightItems: row.maxInFlightItems ?? undefined,
snapshotEnabled: row.snapshotEnabled ?? undefined,
snapshotTtlMs: row.snapshotTtlMs ?? undefined,
};
}

// ---------------------------------------------------------------------------
// Public mapping functions
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -226,27 +254,11 @@ export function mapProjectRow({
const pmType = jiraConfig ? 'jira' : 'trello';

const project: ProjectConfigRaw = {
id: row.id,
orgId: row.orgId,
name: row.name,
repo: row.repo ?? undefined,
baseBranch: row.baseBranch ?? 'main',
branchPrefix: row.branchPrefix ?? 'feature/',
pm: { type: pmType },
model: row.model ?? undefined,
...buildBaseProjectFields(row, pmType),
agentModels: orUndefined(models),
maxIterations: row.maxIterations ?? undefined,
watchdogTimeoutMs: row.watchdogTimeoutMs ?? undefined,
progressModel: row.progressModel ?? undefined,
progressIntervalMinutes: numericOrUndefined(row.progressIntervalMinutes),
workItemBudgetUsd: numericOrUndefined(row.workItemBudgetUsd),
engineSettings: row.agentEngineSettings ?? undefined,
agentEngineSettings: orUndefined(agentEngineSettingsMap) as
| Record<string, EngineSettings>
| undefined,
squintDbUrl: row.squintDbUrl ?? undefined,
runLinksEnabled: row.runLinksEnabled ?? false,
maxInFlightItems: row.maxInFlightItems ?? undefined,
};

if (trelloConfig) {
Expand Down
3 changes: 3 additions & 0 deletions src/db/schema/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export const projects = pgTable(
runLinksEnabled: boolean('run_links_enabled').default(false).notNull(),
maxInFlightItems: integer('max_in_flight_items'),

snapshotEnabled: boolean('snapshot_enabled'),
snapshotTtlMs: integer('snapshot_ttl_ms'),

createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at')
.defaultNow()
Expand Down
12 changes: 12 additions & 0 deletions src/router/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export interface RouterConfig {
// Used for Trello HMAC which includes the full callback URL in the signature.
// Falls back to deriving from request Host header + path at runtime if not set.
webhookCallbackBaseUrl: string | undefined;

// Snapshot defaults (project-level values override these)
snapshotEnabled: boolean;
snapshotDefaultTtlMs: number;
snapshotMaxCount: number;
snapshotMaxSizeBytes: number;
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -120,4 +126,10 @@ export const routerConfig: RouterConfig = {
dockerNetwork: process.env.DOCKER_NETWORK || 'services_default',
emailScheduleIntervalMs: Number(process.env.EMAIL_SCHEDULE_INTERVAL_MS) || 5 * 60 * 1000,
webhookCallbackBaseUrl: process.env.WEBHOOK_CALLBACK_BASE_URL,

// Snapshot defaults — project-level values override these when set
snapshotEnabled: process.env.SNAPSHOT_ENABLED === 'true',
snapshotDefaultTtlMs: Number(process.env.SNAPSHOT_DEFAULT_TTL_MS) || 24 * 60 * 60 * 1000, // 24 hours
snapshotMaxCount: Number(process.env.SNAPSHOT_MAX_COUNT) || 5,
snapshotMaxSizeBytes: Number(process.env.SNAPSHOT_MAX_SIZE_BYTES) || 10 * 1024 * 1024 * 1024, // 10 GB
};
24 changes: 24 additions & 0 deletions tests/unit/db/repositories/configMapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const baseProjectRow = {
agentEngine: null,
agentEngineSettings: null,
runLinksEnabled: false,
maxInFlightItems: null,
snapshotEnabled: null,
snapshotTtlMs: null,
};

const trelloConfig = {
Expand Down Expand Up @@ -346,4 +349,25 @@ describe('mapProjectRow', () => {
const result = mapProjectRow(makeInput({ projectAgentConfigs: agentConfigs }));
expect(Object.hasOwn(result, 'prompts')).toBe(false);
});

it('returns undefined snapshotEnabled and snapshotTtlMs when both are null', () => {
const result = mapProjectRow(makeInput());
expect(result.snapshotEnabled).toBeUndefined();
expect(result.snapshotTtlMs).toBeUndefined();
});

it('maps snapshotEnabled true when set on project row', () => {
const result = mapProjectRow(makeInput({ row: { ...baseProjectRow, snapshotEnabled: true } }));
expect(result.snapshotEnabled).toBe(true);
});

it('maps snapshotEnabled false when explicitly set on project row', () => {
const result = mapProjectRow(makeInput({ row: { ...baseProjectRow, snapshotEnabled: false } }));
expect(result.snapshotEnabled).toBe(false);
});

it('maps snapshotTtlMs when set on project row', () => {
const result = mapProjectRow(makeInput({ row: { ...baseProjectRow, snapshotTtlMs: 3600000 } }));
expect(result.snapshotTtlMs).toBe(3600000);
});
});
49 changes: 49 additions & 0 deletions tests/unit/db/repositories/configRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,4 +458,53 @@ describe('configRepository', () => {
expect(result).toBeUndefined();
});
});

describe('snapshot config mapping', () => {
it('maps snapshotEnabled and snapshotTtlMs from project row', async () => {
const projectWithSnapshot = {
...projectRow,
snapshotEnabled: true,
snapshotTtlMs: 3600000,
};
const mockDb = createSequentialMockDb([[projectWithSnapshot], [], [trelloIntegration]]);
mockGetDb.mockReturnValue(mockDb as never);

const config = await loadConfigFromDb();

const proj = config.projects[0];
expect(proj.snapshotEnabled).toBe(true);
expect(proj.snapshotTtlMs).toBe(3600000);
});

it('leaves snapshotEnabled and snapshotTtlMs undefined when null in DB', async () => {
const mockDb = createSequentialMockDb([[projectRow], [], [trelloIntegration]]);
mockGetDb.mockReturnValue(mockDb as never);

const config = await loadConfigFromDb();

const proj = config.projects[0];
expect(proj.snapshotEnabled).toBeUndefined();
expect(proj.snapshotTtlMs).toBeUndefined();
});

it('maps snapshotEnabled false when explicitly disabled on project', async () => {
const projectWithSnapshotDisabled = {
...projectRow,
snapshotEnabled: false,
snapshotTtlMs: null,
};
const mockDb = createSequentialMockDb([
[projectWithSnapshotDisabled],
[],
[trelloIntegration],
]);
mockGetDb.mockReturnValue(mockDb as never);

const config = await loadConfigFromDb();

const proj = config.projects[0];
expect(proj.snapshotEnabled).toBe(false);
expect(proj.snapshotTtlMs).toBeUndefined();
});
});
});
16 changes: 16 additions & 0 deletions tests/unit/router/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ describe('routerConfig', () => {
it('has default emailScheduleIntervalMs of 5 minutes', () => {
expect(routerConfig.emailScheduleIntervalMs).toBe(5 * 60 * 1000);
});

it('defaults snapshotEnabled to false', () => {
expect(routerConfig.snapshotEnabled).toBe(false);
});

it('defaults snapshotDefaultTtlMs to 24 hours', () => {
expect(routerConfig.snapshotDefaultTtlMs).toBe(24 * 60 * 60 * 1000);
});

it('defaults snapshotMaxCount to 5', () => {
expect(routerConfig.snapshotMaxCount).toBe(5);
});

it('defaults snapshotMaxSizeBytes to 10 GB', () => {
expect(routerConfig.snapshotMaxSizeBytes).toBe(10 * 1024 * 1024 * 1024);
});
});

describe('loadProjectConfig', () => {
Expand Down
Loading