feat(runner): add native persistence contracts (#12169)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agent runs need durable records so Paperclip can explain results and
final status changes.
> - The current heartbeat tables support direct adapters, but they do
not model native runner evidence.
> - The runner transport and server coordinator must share a strict
finalization contract before they write production data.
> - This pull request adds that contract and its additive database
boundary.
> - It does not select the Paperclip Runner or change any existing
adapter execution path.
> - The benefit is a reviewable persistence layer that preserves all
current behavior and supports later guarded integration.

## Linked Issues or Issue Description

Refs #11962

Refs #12129

## What Changed

- Add native run result, finalization, completion, assessment, status
decision, and status effect tables.
- Add inert native metadata to heartbeat runs and events. Keep `legacy`
as the default runtime mode.
- Bind each evidence relationship to one company, issue, run, contract,
result, assessment, and decision with composite constraints.
- Add a strict `paperclip.native_finalization.v1` shared type and
validator.
- Preserve database functions, triggers, and the unique indexes required
by foreign keys in JavaScript backups.
- Add migration, backup, mixed-owner denial, validator, and
direct-adapter compatibility tests.
- Document the new records and their ownership rules.

## Verification

- Run `pnpm -r typecheck`.
- Run `pnpm build`.
- Run `pnpm db:generate`. The schema output and migration safety checks
remain current.
- Run
`PAPERCLIP_PSQL_PATH=/Applications/Postgres.app/Contents/Versions/latest/bin/psql
pnpm exec vitest run
packages/shared/src/validators/native-finalization.test.ts
packages/db/src/client.test.ts packages/db/src/backup-lib.test.ts
server/src/__tests__/heartbeat-workspace-busy.test.ts
server/src/__tests__/heartbeat-comment-wake-batching.test.ts`. All 52
tests pass.
- The full local `pnpm test:run` run completed 4,688 tests. It found 30
existing macOS test-environment failures. A serial rerun with the
canonical `/private/tmp` path reduced those failures to six existing
listener-diagnostics and skill-browser cases. None of those suites use
files in this change.
- The full Linux GitHub Actions matrix passes. This includes all
general-server, serialized-server, workspace, browser, build, typecheck,
canary, and aggregate verification jobs.
- Greptile passes at 5/5. Contributor trust, Superagent, Socket, and
Snyk pass with no finding from this change.
- Storybook visual regression skips by path because this pull request
has no UI or Storybook change.
- Confirm that the diff contains 25 files. Confirm that it contains no
workflow or `pnpm-lock.yaml` changes.

## Risks

- The migration adds tables, columns, indexes, a function, a trigger,
and ownership constraints. It does not remove or rename existing data.
- Composite foreign keys reject mixed-company, mixed-issue, and
mixed-run evidence even when each ID exists.
- The status-version trigger runs only when an issue status changes.
Backup tests confirm that restore retains this trigger and its
dependencies.
- Native source identifiers are unique when present. Legacy event rows
remain unchanged.
- This change does not add a unique run sequence constraint. The later
native writer must allocate its sequence atomically before that
invariant can be safe.
- Existing adapters keep their current execution and finalization paths.
New heartbeat runs default to `legacy` mode.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex with GPT-5. The exact deployment ID and context-window
size are not exposed. The model used agentic reasoning, repository
tools, code execution, and test execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-08-25 13:08:39 -05:00 committed by GitHub
parent 0b01593602
commit 4d2af732ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 43079 additions and 14 deletions

View File

@ -198,6 +198,33 @@ Triage writes serialize on the company and attention-source identity so concurre
`decision_retention` tracks the last observed source `activityAt`, Keep, reversible archive provenance, and monotonic source/archive versions. `decision_archive_notification_outbox` has a unique key over company, source identity, archive version, and immutable origin agent so repeated sweeps cannot enqueue duplicate notifications; delivery claims are retryable and coalesced per agent.
## Native runner persistence
Native runner state is additive to the existing heartbeat tables. Every existing
`heartbeat_runs` row defaults to `runtime_mode = 'legacy'`; adding these columns
does not select the native runtime or start a runner process. Native execution can
record its resolved runtime profile, provider session, driver, completion
contract, durable event cursor, and finalization phase on the run when a later
rollout explicitly selects it.
`completion_contracts`, `native_run_results`, `native_run_finalizations`,
`work_assessments`, `status_decisions`, and `status_decision_effects` form the
append-oriented evidence and status-decision chain. Unique fingerprints,
versions, ordinals, and idempotency keys make retries deterministic. Composite
foreign keys bind every contract, result, assessment, decision, effect, and
finalization to one company, issue, and run. The database rejects mixed-owner
evidence even when every referenced ID exists. Native source identities on
`heartbeat_run_events` are nullable so legacy events remain readable without
rewriting historical rows. Per-run native source identifiers are unique, while
the existing legacy sequence behavior remains unchanged until an atomic event
allocator is introduced with the native writer.
Issue `status_version` advances only when `status` changes. The JavaScript backup
path includes user-defined functions and triggers so a restored database keeps
that invariant. Removing or disabling a future native rollout flag must not
delete these records; persisted experimental runs remain available for recovery
and inspection.
## Plugin database namespaces
The plugin runtime tracks plugin-owned database namespaces and migrations in `plugin_database_namespaces` and `plugin_migrations`. Hosted deployments that separate runtime and migration connections should set `DATABASE_MIGRATION_URL`; plugin namespace migration work uses the migration connection when present.

View File

@ -140,6 +140,21 @@ describeEmbeddedPostgres("runDatabaseBackup", () => {
"created_at" timestamptz NOT NULL DEFAULT now()
);
`);
await sourceSql.unsafe(`
CREATE FUNCTION "public"."backup_test_mark_done"()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW."state" := 'done';
RETURN NEW;
END;
$$;
CREATE TRIGGER "backup_test_mark_done_trigger"
BEFORE UPDATE OF "title" ON "public"."backup_test_records"
FOR EACH ROW
EXECUTE FUNCTION "public"."backup_test_mark_done"();
`);
const payload = "x".repeat(8192);
for (let index = 0; index < 160; index += 1) {
@ -213,6 +228,18 @@ describeEmbeddedPostgres("runDatabaseBackup", () => {
metadata: { index: 159, even: false },
},
]);
await restoreSql.unsafe(`
UPDATE "public"."backup_test_records"
SET "title" = 'triggered'
WHERE "title" = 'row-0'
`);
const triggeredRows = await restoreSql.unsafe<{ state: string }[]>(`
SELECT "state"::text AS "state"
FROM "public"."backup_test_records"
WHERE "title" = 'triggered'
`);
expect(triggeredRows).toEqual([{ state: "done" }]);
} finally {
await sourceSql.end();
await restoreSql.end();

View File

@ -591,6 +591,7 @@ export async function runDatabaseBackup(opts: RunDatabaseBackupOptions): Promise
emitStatement("BEGIN;");
emitStatement("SET LOCAL session_replication_role = replica;");
emitStatement("SET LOCAL client_min_messages = warning;");
emitStatement("SET LOCAL check_function_bodies = false;");
emit("");
const allTables = await sql<TableDefinition[]>`
@ -809,7 +810,9 @@ export async function runDatabaseBackup(opts: RunDatabaseBackupOptions): Promise
emit("");
}
// Foreign keys (after all tables and referenced unique constraints are created)
// Collect foreign keys now. Emit them after routines and standalone indexes
// because PostgreSQL permits a non-constraint unique index to be the target
// of a foreign key.
const allForeignKeys = await sql<{
constraint_name: string;
source_schema: string;
@ -849,14 +852,29 @@ export async function runDatabaseBackup(opts: RunDatabaseBackupOptions): Promise
&& includedTableNames.has(tableKey(fk.target_schema, fk.target_table)),
);
if (fks.length > 0) {
emit("-- Foreign keys");
for (const fk of fks) {
const srcCols = fk.source_columns.map((c) => `"${c}"`).join(", ");
const tgtCols = fk.target_columns.map((c) => `"${c}"`).join(", ");
emitStatement(
`ALTER TABLE ${quoteQualifiedName(fk.source_schema, fk.source_table)} ADD CONSTRAINT "${fk.constraint_name}" FOREIGN KEY (${srcCols}) REFERENCES ${quoteQualifiedName(fk.target_schema, fk.target_table)} (${tgtCols}) ON UPDATE ${fk.update_rule} ON DELETE ${fk.delete_rule};`,
);
// JavaScript backups are used when a worktree seed filters or transforms
// table data. Preserve user-defined routines before indexes because an
// expression index may depend on a user-defined function.
const routines = await sql<{ definition: string }[]>`
SELECT pg_get_functiondef(p.oid) AS definition
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE ${sql.unsafe(nonSystemSchemaPredicate("n.nspname"))}
AND p.prokind IN ('f', 'p')
AND NOT EXISTS (
SELECT 1
FROM pg_depend d
WHERE d.classid = 'pg_proc'::regclass
AND d.objid = p.oid
AND d.deptype = 'e'
)
ORDER BY n.nspname, p.proname, pg_get_function_identity_arguments(p.oid)
`;
if (routines.length > 0) {
emit("-- Functions and procedures");
for (const routine of routines) {
const definition = routine.definition.trimEnd();
emitStatement(definition.endsWith(";") ? definition : `${definition};`);
}
emit("");
}
@ -883,6 +901,18 @@ export async function runDatabaseBackup(opts: RunDatabaseBackupOptions): Promise
emit("");
}
if (fks.length > 0) {
emit("-- Foreign keys");
for (const fk of fks) {
const srcCols = fk.source_columns.map((c) => `"${c}"`).join(", ");
const tgtCols = fk.target_columns.map((c) => `"${c}"`).join(", ");
emitStatement(
`ALTER TABLE ${quoteQualifiedName(fk.source_schema, fk.source_table)} ADD CONSTRAINT "${fk.constraint_name}" FOREIGN KEY (${srcCols}) REFERENCES ${quoteQualifiedName(fk.target_schema, fk.target_table)} (${tgtCols}) ON UPDATE ${fk.update_rule} ON DELETE ${fk.delete_rule};`,
);
}
emit("");
}
// Dump data for each table
for (const { schema_name, tablename } of tables) {
const currentTableKey = tableKey(schema_name, tablename);
@ -938,6 +968,33 @@ export async function runDatabaseBackup(opts: RunDatabaseBackupOptions): Promise
emit("");
}
const allTriggers = await sql<{
schema_name: string;
tablename: string;
definition: string;
}[]>`
SELECT
n.nspname AS schema_name,
c.relname AS tablename,
pg_get_triggerdef(t.oid, true) AS definition
FROM pg_trigger t
JOIN pg_class c ON c.oid = t.tgrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE NOT t.tgisinternal
AND ${sql.unsafe(nonSystemSchemaPredicate("n.nspname"))}
ORDER BY n.nspname, c.relname, t.tgname
`;
const triggers = allTriggers.filter((entry) => (
includedTableNames.has(tableKey(entry.schema_name, entry.tablename))
));
if (triggers.length > 0) {
emit("-- Triggers");
for (const trigger of triggers) {
emitStatement(`${trigger.definition};`);
}
emit("");
}
// Sequence values
if (sequences.length > 0) {
emit("-- Sequence values");

View File

@ -1405,4 +1405,466 @@ describeEmbeddedPostgres("applyPendingMigrations", () => {
},
20_000,
);
it(
"preserves legacy runs while adding native persistence and replay-safe status versioning",
async () => {
const connectionString = await createTempDatabase();
await applyPendingMigrations(connectionString);
const nativePersistenceHash = await migrationHash("0227_modern_pandemic.sql");
const sql = postgres(connectionString, { max: 1, onnotice: () => {} });
const companyId = "10000000-0000-4000-8000-000000000227";
const agentId = "20000000-0000-4000-8000-000000000227";
const runId = "30000000-0000-4000-8000-000000000227";
const issueId = "40000000-0000-4000-8000-000000000227";
const contractId = "50000000-0000-4000-8000-000000000227";
const resultId = "60000000-0000-4000-8000-000000000227";
const assessmentId = "70000000-0000-4000-8000-000000000227";
const decisionId = "80000000-0000-4000-8000-000000000227";
const otherCompanyId = "11000000-0000-4000-8000-000000000227";
const otherAgentId = "21000000-0000-4000-8000-000000000227";
const otherRunId = "31000000-0000-4000-8000-000000000227";
const otherIssueId = "41000000-0000-4000-8000-000000000227";
const otherContractId = "51000000-0000-4000-8000-000000000227";
const otherResultId = "61000000-0000-4000-8000-000000000227";
const otherAssessmentId = "71000000-0000-4000-8000-000000000227";
const otherDecisionId = "81000000-0000-4000-8000-000000000227";
try {
await sql.unsafe(`
DROP TABLE IF EXISTS status_decision_effects, status_decisions, work_assessments,
native_run_finalizations, native_run_results, completion_contracts CASCADE;
DROP TRIGGER IF EXISTS paperclip_issue_status_version_trigger ON issues;
DROP FUNCTION IF EXISTS paperclip_bump_issue_status_version();
DROP INDEX IF EXISTS issues_company_id_uq;
DROP INDEX IF EXISTS heartbeat_run_events_run_source_event_uq;
DROP INDEX IF EXISTS heartbeat_run_events_run_source_seq_uq;
ALTER TABLE heartbeat_run_events
DROP COLUMN IF EXISTS source_instance_id,
DROP COLUMN IF EXISTS source_event_id,
DROP COLUMN IF EXISTS source_seq,
DROP COLUMN IF EXISTS source_payload_sha256,
DROP COLUMN IF EXISTS protocol_schema_version;
ALTER TABLE heartbeat_run_events ALTER COLUMN seq TYPE integer;
ALTER TABLE heartbeat_runs
DROP COLUMN IF EXISTS runtime_mode,
DROP COLUMN IF EXISTS runtime_mode_resolver_version,
DROP COLUMN IF EXISTS runtime_mode_reason,
DROP COLUMN IF EXISTS runtime_mode_resolved_at,
DROP COLUMN IF EXISTS runner_profile_json,
DROP COLUMN IF EXISTS runner_instance_id,
DROP COLUMN IF EXISTS native_session_id,
DROP COLUMN IF EXISTS native_issue_id,
DROP COLUMN IF EXISTS driver_kind,
DROP COLUMN IF EXISTS driver_version,
DROP COLUMN IF EXISTS completion_contract_id,
DROP COLUMN IF EXISTS completion_contract_sha256,
DROP COLUMN IF EXISTS next_event_seq,
DROP COLUMN IF EXISTS native_phase,
DROP COLUMN IF EXISTS native_phase_updated_at;
ALTER TABLE issues
DROP COLUMN IF EXISTS status_version,
DROP COLUMN IF EXISTS last_status_decision_id;
`);
await sql`DELETE FROM "drizzle"."__drizzle_migrations" WHERE "hash" = ${nativePersistenceHash}`;
await sql`
INSERT INTO companies (id, name, issue_prefix)
VALUES (${companyId}, 'Native persistence fixture', 'NPF')
`;
await sql`
INSERT INTO agents (id, company_id, name)
VALUES (${agentId}, ${companyId}, 'Legacy migration agent')
`;
await sql`
INSERT INTO heartbeat_runs (id, company_id, agent_id, status)
VALUES (${runId}, ${companyId}, ${agentId}, 'succeeded')
`;
await sql`
INSERT INTO issues (id, company_id, title, status)
VALUES (${issueId}, ${companyId}, 'Legacy migration issue', 'in_progress')
`;
await sql.unsafe(`
INSERT INTO heartbeat_run_events
(company_id, run_id, agent_id, seq, event_type, stream, level, message, payload, created_at)
VALUES
('${companyId}', '${runId}', '${agentId}', 1, 'legacy.start', 'system', 'info', 'one', '{"bytes":"alpha-1"}'::jsonb, '2026-08-01T00:00:01.000Z'),
('${companyId}', '${runId}', '${agentId}', 5, 'legacy.log', 'stdout', 'info', 'first-five', '{"bytes":"beta-5a"}'::jsonb, '2026-08-01T00:00:02.000Z'),
('${companyId}', '${runId}', '${agentId}', 5, 'legacy.log', 'stderr', 'warn', 'duplicate-five', '{"bytes":"gamma-5b"}'::jsonb, '2026-08-01T00:00:03.000Z'),
('${companyId}', '${runId}', '${agentId}', 9, 'legacy.end', 'system', 'info', 'nine', '{"bytes":"delta-9"}'::jsonb, '2026-08-01T00:00:04.000Z')
`);
} finally {
await sql.end();
}
await applyPendingMigrations(connectionString);
const verifySql = postgres(connectionString, { max: 1, onnotice: () => {} });
try {
const events = await verifySql.unsafe<{
seq: string;
event_type: string;
stream: string;
level: string;
message: string;
payload: { bytes: string };
created_at: Date;
}[]>(`
SELECT seq, event_type, stream, level, message, payload, created_at
FROM heartbeat_run_events
WHERE run_id = '${runId}'
ORDER BY id
`);
expect(events.map((event) => Number(event.seq))).toEqual([1, 5, 5, 9]);
expect(events.map(({ seq: _seq, ...event }) => ({
...event,
created_at: event.created_at.toISOString(),
}))).toEqual([
{
event_type: "legacy.start",
stream: "system",
level: "info",
message: "one",
payload: { bytes: "alpha-1" },
created_at: "2026-08-01T00:00:01.000Z",
},
{
event_type: "legacy.log",
stream: "stdout",
level: "info",
message: "first-five",
payload: { bytes: "beta-5a" },
created_at: "2026-08-01T00:00:02.000Z",
},
{
event_type: "legacy.log",
stream: "stderr",
level: "warn",
message: "duplicate-five",
payload: { bytes: "gamma-5b" },
created_at: "2026-08-01T00:00:03.000Z",
},
{
event_type: "legacy.end",
stream: "system",
level: "info",
message: "nine",
payload: { bytes: "delta-9" },
created_at: "2026-08-01T00:00:04.000Z",
},
]);
const runs = await verifySql.unsafe<{ runtime_mode: string; next_event_seq: string }[]>(`
SELECT runtime_mode, next_event_seq
FROM heartbeat_runs
WHERE id = '${runId}'
`);
expect(runs.map((run) => ({
runtimeMode: run.runtime_mode,
nextEventSeq: Number(run.next_event_seq),
}))).toEqual([{ runtimeMode: "legacy", nextEventSeq: 10 }]);
const nativeRowsBefore = await verifySql.unsafe<{ table_name: string; row_count: number }[]>(`
SELECT 'completion_contracts' AS table_name, count(*)::int AS row_count FROM completion_contracts
UNION ALL SELECT 'native_run_results', count(*)::int FROM native_run_results
UNION ALL SELECT 'native_run_finalizations', count(*)::int FROM native_run_finalizations
UNION ALL SELECT 'work_assessments', count(*)::int FROM work_assessments
UNION ALL SELECT 'status_decisions', count(*)::int FROM status_decisions
UNION ALL SELECT 'status_decision_effects', count(*)::int FROM status_decision_effects
ORDER BY table_name
`);
expect(nativeRowsBefore.every((row) => row.row_count === 0)).toBe(true);
await verifySql`
INSERT INTO companies (id, name, issue_prefix)
VALUES (${otherCompanyId}, 'Other native persistence fixture', 'ONP')
`;
await verifySql`
INSERT INTO agents (id, company_id, name)
VALUES (${otherAgentId}, ${otherCompanyId}, 'Other native migration agent')
`;
await verifySql`
INSERT INTO heartbeat_runs (
id, company_id, agent_id, status, native_issue_id, completion_contract_id
) VALUES (
${otherRunId}, ${otherCompanyId}, ${otherAgentId}, 'succeeded',
${otherIssueId}, ${otherContractId}
)
`;
await verifySql`
INSERT INTO issues (id, company_id, title, status)
VALUES (${otherIssueId}, ${otherCompanyId}, 'Other native issue', 'in_progress')
`;
await verifySql`
UPDATE heartbeat_runs
SET native_issue_id = ${issueId}, completion_contract_id = ${contractId}
WHERE id = ${runId}
`;
await expect(verifySql`
INSERT INTO completion_contracts (
company_id, issue_id, revision, schema_version, policy_version,
risk, completion_authority, incomplete_criteria_policy, contract_json,
canonical_sha256, created_by_actor_type, created_by_actor_id
) VALUES (
${companyId}, ${otherIssueId}, 1, 'paperclip.completion-contract.v1',
'policy-v1', 'low', 'server', 'review', ${JSON.stringify({ criteria: [] })}::jsonb,
'cross-company-contract-sha', 'system', 'migration-test'
)
`).rejects.toThrow(/completion_contracts_issue_company_fk/);
await verifySql`
INSERT INTO completion_contracts (
id, company_id, issue_id, revision, schema_version, policy_version,
risk, completion_authority, incomplete_criteria_policy, contract_json,
canonical_sha256, created_by_actor_type, created_by_actor_id
) VALUES (
${contractId}, ${companyId}, ${issueId}, 1, 'paperclip.completion-contract.v1',
'policy-v1', 'low', 'server', 'review', ${JSON.stringify({ criteria: [] })}::jsonb,
'contract-sha', 'system', 'migration-test'
)
`;
await verifySql`
INSERT INTO completion_contracts (
id, company_id, issue_id, revision, schema_version, policy_version,
risk, completion_authority, incomplete_criteria_policy, contract_json,
canonical_sha256, created_by_actor_type, created_by_actor_id
) VALUES (
${otherContractId}, ${otherCompanyId}, ${otherIssueId}, 1,
'paperclip.completion-contract.v1', 'policy-v1', 'low', 'server', 'review',
${JSON.stringify({ criteria: [] })}::jsonb, 'other-contract-sha',
'system', 'migration-test'
)
`;
await expect(verifySql`
INSERT INTO native_run_results (
company_id, issue_id, run_id, completion_contract_id,
server_fingerprint, schema_status, result_json, canonical_sha256
) VALUES (
${companyId}, ${issueId}, ${otherRunId}, ${contractId},
'cross-company-run', 'valid', ${JSON.stringify({ summary: "invalid" })}::jsonb,
'cross-company-run-sha'
)
`).rejects.toThrow(/native_run_results_run_contract_owner_fk/);
await expect(verifySql`
INSERT INTO native_run_results (
company_id, issue_id, run_id, completion_contract_id,
server_fingerprint, schema_status, result_json, canonical_sha256
) VALUES (
${companyId}, ${issueId}, ${runId}, ${otherContractId},
'cross-company-contract', 'valid', ${JSON.stringify({ summary: "invalid" })}::jsonb,
'cross-company-contract-sha'
)
`).rejects.toThrow(/native_run_results_run_contract_owner_fk/);
await verifySql`
INSERT INTO native_run_results (
id, company_id, issue_id, run_id, completion_contract_id,
server_fingerprint, schema_status, result_json, canonical_sha256
) VALUES (
${resultId}, ${companyId}, ${issueId}, ${runId}, ${contractId},
'result-fingerprint', 'valid', ${JSON.stringify({ summary: "done" })}::jsonb, 'result-sha'
)
`;
await verifySql`
INSERT INTO native_run_results (
id, company_id, issue_id, run_id, completion_contract_id,
server_fingerprint, schema_status, result_json, canonical_sha256
) VALUES (
${otherResultId}, ${otherCompanyId}, ${otherIssueId}, ${otherRunId},
${otherContractId}, 'other-result-fingerprint', 'valid',
${JSON.stringify({ summary: "other" })}::jsonb, 'other-result-sha'
)
`;
await expect(verifySql`
INSERT INTO work_assessments (
company_id, issue_id, run_id, contract_id, result_id,
trigger_kind, trigger_actor_company_id, prior_issue_status,
prior_status_version, policy_version, assessment_json, input_digest
) VALUES (
${companyId}, ${issueId}, ${runId}, ${contractId}, ${otherResultId},
'run_terminal', ${companyId}, 'in_progress', 0, 'policy-v1',
${JSON.stringify({ disposition: "invalid" })}::jsonb,
'cross-company-assessment-input-sha'
)
`).rejects.toThrow(/work_assessments_result_owner_fk/);
await expect(verifySql`
INSERT INTO work_assessments (
company_id, issue_id, run_id, contract_id, result_id,
trigger_kind, trigger_actor_company_id, prior_issue_status,
prior_status_version, policy_version, assessment_json, input_digest
) VALUES (
${companyId}, ${issueId}, ${runId}, ${contractId}, ${resultId},
'run_terminal', ${otherCompanyId}, 'in_progress', 0, 'policy-v1',
${JSON.stringify({ disposition: "invalid" })}::jsonb,
'cross-company-trigger-input-sha'
)
`).rejects.toThrow(/work_assessments_trigger_actor_company_check/);
await verifySql`
INSERT INTO work_assessments (
id, company_id, issue_id, run_id, contract_id, result_id,
trigger_kind, trigger_actor_company_id, prior_issue_status,
prior_status_version, policy_version, assessment_json, input_digest
) VALUES (
${assessmentId}, ${companyId}, ${issueId}, ${runId}, ${contractId}, ${resultId},
'run_terminal', ${companyId}, 'in_progress', 0, 'policy-v1',
${JSON.stringify({ disposition: "done" })}::jsonb, 'assessment-input-sha'
)
`;
await verifySql`
INSERT INTO work_assessments (
id, company_id, issue_id, run_id, contract_id, result_id,
trigger_kind, trigger_actor_company_id, prior_issue_status,
prior_status_version, policy_version, assessment_json, input_digest
) VALUES (
${otherAssessmentId}, ${otherCompanyId}, ${otherIssueId}, ${otherRunId},
${otherContractId}, ${otherResultId}, 'run_terminal', ${otherCompanyId},
'in_progress', 0, 'policy-v1',
${JSON.stringify({ disposition: "done" })}::jsonb, 'other-assessment-input-sha'
)
`;
await expect(verifySql`
INSERT INTO status_decisions (
company_id, issue_id, run_id, assessment_id, decision_version,
policy_version, from_status, to_status, reason_code,
decision_json, decision_digest
) VALUES (
${companyId}, ${issueId}, ${runId}, ${otherAssessmentId}, 1,
'policy-v1', 'in_progress', 'done', 'native_result_accepted',
${JSON.stringify({ toStatus: "done" })}::jsonb, 'cross-company-decision-sha'
)
`).rejects.toThrow(/status_decisions_assessment_owner_fk/);
await verifySql`
INSERT INTO status_decisions (
id, company_id, issue_id, run_id, assessment_id, decision_version,
policy_version, from_status, to_status, reason_code,
decision_json, decision_digest
) VALUES (
${decisionId}, ${companyId}, ${issueId}, ${runId}, ${assessmentId}, 1,
'policy-v1', 'in_progress', 'done', 'native_result_accepted',
${JSON.stringify({ toStatus: "done" })}::jsonb, 'decision-sha'
)
`;
await verifySql`
INSERT INTO status_decisions (
id, company_id, issue_id, run_id, assessment_id, decision_version,
policy_version, from_status, to_status, reason_code,
decision_json, decision_digest
) VALUES (
${otherDecisionId}, ${otherCompanyId}, ${otherIssueId}, ${otherRunId},
${otherAssessmentId}, 1, 'policy-v1', 'in_progress', 'done',
'native_result_accepted', ${JSON.stringify({ toStatus: "done" })}::jsonb,
'other-decision-sha'
)
`;
await expect(verifySql`
INSERT INTO status_decision_effects (
company_id, issue_id, decision_id, ordinal, effect_kind,
target_type, idempotency_key, payload
) VALUES (
${companyId}, ${issueId}, ${otherDecisionId}, 0, 'update_issue_status',
'issue', 'cross-company-decision-effect', ${JSON.stringify({ status: "done" })}::jsonb
)
`).rejects.toThrow(/status_decision_effects_decision_owner_fk/);
await verifySql`
INSERT INTO status_decision_effects (
company_id, issue_id, decision_id, ordinal, effect_kind,
target_type, idempotency_key, payload
) VALUES (
${companyId}, ${issueId}, ${decisionId}, 0, 'update_issue_status',
'issue', 'decision-effect-1', ${JSON.stringify({ status: "done" })}::jsonb
)
`;
await expect(verifySql`
INSERT INTO native_run_finalizations (
run_id, company_id, issue_id, phase, result_id, assessment_id, decision_id
) VALUES (
${runId}, ${companyId}, ${issueId}, 'committed', ${resultId},
${assessmentId}, ${otherDecisionId}
)
`).rejects.toThrow(/native_run_finalizations_decision_owner_fk/);
await verifySql`
INSERT INTO native_run_finalizations (
run_id, company_id, issue_id, phase, result_id, assessment_id, decision_id
) VALUES (
${runId}, ${companyId}, ${issueId}, 'committed', ${resultId}, ${assessmentId}, ${decisionId}
)
`;
await verifySql`UPDATE issues SET title = 'Renamed legacy issue' WHERE id = ${issueId}`;
await verifySql`UPDATE issues SET status = 'done' WHERE id = ${issueId}`;
const issues = await verifySql.unsafe<{ status: string; status_version: string }[]>(`
SELECT status, status_version
FROM issues
WHERE id = '${issueId}'
`);
expect(issues.map((issue) => ({
status: issue.status,
statusVersion: Number(issue.status_version),
}))).toEqual([{ status: "done", statusVersion: 1 }]);
await verifySql`DELETE FROM "drizzle"."__drizzle_migrations" WHERE "hash" = ${nativePersistenceHash}`;
} finally {
await verifySql.end();
}
await expect(applyPendingMigrations(connectionString)).resolves.toBeUndefined();
await expect(inspectMigrations(connectionString)).resolves.toMatchObject({
status: "upToDate",
});
const replaySql = postgres(connectionString, { max: 1, onnotice: () => {} });
try {
const replayed = await replaySql.unsafe<{
status_version: string;
next_event_seq: string;
trigger_count: number;
finalization_count: number;
}[]>(`
SELECT
issue.status_version,
run.next_event_seq,
(
SELECT count(*)::int
FROM pg_trigger
WHERE tgname = 'paperclip_issue_status_version_trigger'
AND NOT tgisinternal
) AS trigger_count,
(
SELECT count(*)::int
FROM native_run_finalizations
WHERE run_id = '${runId}'
) AS finalization_count
FROM issues issue
CROSS JOIN heartbeat_runs run
WHERE issue.id = '${issueId}' AND run.id = '${runId}'
`);
expect(replayed.map((row) => ({
statusVersion: Number(row.status_version),
nextEventSeq: Number(row.next_event_seq),
triggerCount: row.trigger_count,
finalizationCount: row.finalization_count,
}))).toEqual([{
statusVersion: 1,
nextEventSeq: 10,
triggerCount: 1,
finalizationCount: 1,
}]);
} finally {
await replaySql.end();
}
},
60_000,
);
});

View File

@ -411,6 +411,25 @@ async function columnExists(
return rows[0]?.exists ?? false;
}
async function columnHasDataType(
sql: ReturnType<typeof postgres>,
tableName: string,
columnName: string,
dataType: string,
): Promise<boolean> {
const rows = await sql<{ dataType: string; udtName: string }[]>`
SELECT data_type AS "dataType", udt_name AS "udtName"
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = ${tableName}
AND column_name = ${columnName}
`;
const expected = dataType.toLowerCase();
return rows.some((row) => (
row.dataType.toLowerCase() === expected || row.udtName.toLowerCase() === expected
));
}
async function indexExists(
sql: ReturnType<typeof postgres>,
indexName: string,
@ -444,11 +463,65 @@ async function constraintExists(
return rows[0]?.exists ?? false;
}
async function functionExists(
sql: ReturnType<typeof postgres>,
functionName: string,
): Promise<boolean> {
const rows = await sql<{ exists: boolean }[]>`
SELECT EXISTS (
SELECT 1
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'public'
AND p.proname = ${functionName}
) AS exists
`;
return rows[0]?.exists ?? false;
}
async function triggerExists(
sql: ReturnType<typeof postgres>,
triggerName: string,
): Promise<boolean> {
const rows = await sql<{ exists: boolean }[]>`
SELECT EXISTS (
SELECT 1
FROM pg_trigger t
JOIN pg_class c ON c.oid = t.tgrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public'
AND t.tgname = ${triggerName}
AND NOT t.tgisinternal
) AS exists
`;
return rows[0]?.exists ?? false;
}
async function heartbeatNextEventSequencesAreCurrent(
sql: ReturnType<typeof postgres>,
): Promise<boolean> {
const rows = await sql<{ current: boolean }[]>`
SELECT NOT EXISTS (
SELECT 1
FROM heartbeat_runs run
WHERE run.next_event_seq IS DISTINCT FROM COALESCE((
SELECT max(event.seq) + 1
FROM heartbeat_run_events event
WHERE event.run_id = run.id
), 1)
) AS current
`;
return rows[0]?.current ?? false;
}
async function migrationStatementAlreadyApplied(
sql: ReturnType<typeof postgres>,
statement: string,
): Promise<boolean> {
const normalized = statement.replace(/\s+/g, " ").trim();
const normalized = statement
.replace(/^\s*--.*$/gm, "")
.replace(/\s+/g, " ")
.trim();
const createTableMatch = normalized.match(/^CREATE TABLE(?: IF NOT EXISTS)? "([^"]+)"/i);
if (createTableMatch) {
@ -462,6 +535,18 @@ async function migrationStatementAlreadyApplied(
return columnExists(sql, addColumnMatch[1], addColumnMatch[2]);
}
const alterColumnTypeMatch = normalized.match(
/^ALTER TABLE "([^"]+)" ALTER COLUMN "([^"]+)" SET DATA TYPE ([A-Za-z0-9_]+)/i,
);
if (alterColumnTypeMatch) {
return columnHasDataType(
sql,
alterColumnTypeMatch[1],
alterColumnTypeMatch[2],
alterColumnTypeMatch[3],
);
}
const createIndexMatch = normalized.match(/^CREATE (?:UNIQUE )?INDEX(?: IF NOT EXISTS)? "([^"]+)"/i);
if (createIndexMatch) {
return indexExists(sql, createIndexMatch[1]);
@ -472,6 +557,30 @@ async function migrationStatementAlreadyApplied(
return constraintExists(sql, addConstraintMatch[2]);
}
const createFunctionMatch = normalized.match(
/^CREATE OR REPLACE FUNCTION "?([A-Za-z_][A-Za-z0-9_]*)"?\s*\(/i,
);
if (createFunctionMatch) {
return functionExists(sql, createFunctionMatch[1]);
}
const createTriggerMatch = normalized.match(
/^CREATE TRIGGER "?([A-Za-z_][A-Za-z0-9_]*)"?/i,
);
if (createTriggerMatch) {
return triggerExists(sql, createTriggerMatch[1]);
}
// This native-runner cursor backfill has a persistent postcondition. Verify it
// instead of replaying it when a restored database is missing only the
// migration-history row.
if (
normalized.startsWith('UPDATE "heartbeat_runs" AS run')
&& normalized.includes('SET "next_event_seq" = COALESCE')
) {
return heartbeatNextEventSequencesAreCurrent(sql);
}
// If we cannot reason about a statement safely, require manual migration.
return false;
}

View File

@ -0,0 +1,410 @@
CREATE TABLE IF NOT EXISTS "completion_contracts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"revision" integer NOT NULL,
"schema_version" text NOT NULL,
"policy_version" text NOT NULL,
"risk" text NOT NULL,
"completion_authority" text NOT NULL,
"incomplete_criteria_policy" text NOT NULL,
"contract_json" jsonb NOT NULL,
"canonical_sha256" text NOT NULL,
"created_by_actor_type" text NOT NULL,
"created_by_actor_id" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"supersedes_contract_id" uuid
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "native_run_finalizations" (
"run_id" uuid PRIMARY KEY NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"phase" text NOT NULL,
"attempt" integer DEFAULT 0 NOT NULL,
"lease_owner" text,
"lease_expires_at" timestamp with time zone,
"result_id" uuid,
"assessment_id" uuid,
"decision_id" uuid,
"failure_code" text,
"failure_detail" jsonb,
"next_attempt_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "native_run_results" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"run_id" uuid NOT NULL,
"turn_id" text,
"completion_contract_id" uuid NOT NULL,
"caller_result_id" text,
"caller_dedupe_key" text,
"server_fingerprint" text NOT NULL,
"schema_status" text NOT NULL,
"rejection_code" text,
"result_json" jsonb NOT NULL,
"canonical_sha256" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "status_decision_effects" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"decision_id" uuid NOT NULL,
"ordinal" integer NOT NULL,
"effect_kind" text NOT NULL,
"target_type" text NOT NULL,
"target_id" text,
"idempotency_key" text NOT NULL,
"payload" jsonb NOT NULL,
"delivery_state" text DEFAULT 'pending' NOT NULL,
"attempt_count" integer DEFAULT 0 NOT NULL,
"next_attempt_at" timestamp with time zone,
"last_error" text,
"delivered_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "status_decisions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"run_id" uuid NOT NULL,
"assessment_id" uuid NOT NULL,
"decision_version" bigint NOT NULL,
"policy_version" text NOT NULL,
"from_status" text NOT NULL,
"to_status" text NOT NULL,
"reason_code" text NOT NULL,
"decision_json" jsonb NOT NULL,
"decision_digest" text NOT NULL,
"application_state" text DEFAULT 'proposed' NOT NULL,
"supersedes_decision_id" uuid,
"applied_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "work_assessments" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"run_id" uuid NOT NULL,
"turn_id" text,
"contract_id" uuid NOT NULL,
"result_id" uuid NOT NULL,
"trigger_kind" text NOT NULL,
"trigger_ref" text,
"trigger_capability" text,
"trigger_actor_company_id" uuid NOT NULL,
"prior_issue_status" text NOT NULL,
"prior_status_version" bigint NOT NULL,
"prior_decision_id" uuid,
"policy_version" text NOT NULL,
"assessment_json" jsonb NOT NULL,
"input_digest" text NOT NULL,
"supersedes_assessment_id" uuid,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "heartbeat_run_events" ALTER COLUMN "seq" SET DATA TYPE bigint;--> statement-breakpoint
ALTER TABLE "heartbeat_run_events" ADD COLUMN IF NOT EXISTS "source_instance_id" text;--> statement-breakpoint
ALTER TABLE "heartbeat_run_events" ADD COLUMN IF NOT EXISTS "source_event_id" text;--> statement-breakpoint
ALTER TABLE "heartbeat_run_events" ADD COLUMN IF NOT EXISTS "source_seq" bigint;--> statement-breakpoint
ALTER TABLE "heartbeat_run_events" ADD COLUMN IF NOT EXISTS "source_payload_sha256" text;--> statement-breakpoint
ALTER TABLE "heartbeat_run_events" ADD COLUMN IF NOT EXISTS "protocol_schema_version" integer;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "runtime_mode" text DEFAULT 'legacy' NOT NULL;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "runtime_mode_resolver_version" text;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "runtime_mode_reason" text;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "runtime_mode_resolved_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "runner_profile_json" jsonb;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "runner_instance_id" uuid;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "native_session_id" uuid;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "native_issue_id" uuid;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "driver_kind" text;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "driver_version" text;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "completion_contract_id" uuid;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "completion_contract_sha256" text;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "next_event_seq" bigint DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "native_phase" text;--> statement-breakpoint
ALTER TABLE "heartbeat_runs" ADD COLUMN IF NOT EXISTS "native_phase_updated_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "status_decisions" ADD COLUMN IF NOT EXISTS "run_id" uuid;--> statement-breakpoint
ALTER TABLE "status_decisions" ALTER COLUMN "run_id" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "issues" ADD COLUMN IF NOT EXISTS "status_version" bigint DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE "issues" ADD COLUMN IF NOT EXISTS "last_status_decision_id" uuid;--> statement-breakpoint
UPDATE "heartbeat_runs" AS run
SET "next_event_seq" = COALESCE((
SELECT max(event."seq") + 1
FROM "heartbeat_run_events" AS event
WHERE event."run_id" = run."id"
), 1);--> statement-breakpoint
CREATE OR REPLACE FUNCTION paperclip_bump_issue_status_version()
RETURNS trigger AS $$
BEGIN
IF NEW."status" IS DISTINCT FROM OLD."status" THEN
NEW."status_version" := OLD."status_version" + 1;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;--> statement-breakpoint
DROP TRIGGER IF EXISTS paperclip_issue_status_version_trigger ON "issues";--> statement-breakpoint
CREATE TRIGGER paperclip_issue_status_version_trigger
BEFORE UPDATE OF "status" ON "issues"
FOR EACH ROW EXECUTE FUNCTION paperclip_bump_issue_status_version();--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "completion_contracts" ADD CONSTRAINT "completion_contracts_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "completion_contracts" ADD CONSTRAINT "completion_contracts_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_result_id_native_run_results_id_fk" FOREIGN KEY ("result_id") REFERENCES "public"."native_run_results"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_results" ADD CONSTRAINT "native_run_results_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_results" ADD CONSTRAINT "native_run_results_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_results" ADD CONSTRAINT "native_run_results_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_results" ADD CONSTRAINT "native_run_results_completion_contract_id_completion_contracts_id_fk" FOREIGN KEY ("completion_contract_id") REFERENCES "public"."completion_contracts"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "status_decision_effects" ADD CONSTRAINT "status_decision_effects_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "status_decision_effects" ADD CONSTRAINT "status_decision_effects_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "status_decision_effects" ADD CONSTRAINT "status_decision_effects_decision_id_status_decisions_id_fk" FOREIGN KEY ("decision_id") REFERENCES "public"."status_decisions"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "status_decisions" ADD CONSTRAINT "status_decisions_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "status_decisions" ADD CONSTRAINT "status_decisions_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "status_decisions" ADD CONSTRAINT "status_decisions_assessment_id_work_assessments_id_fk" FOREIGN KEY ("assessment_id") REFERENCES "public"."work_assessments"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_contract_id_completion_contracts_id_fk" FOREIGN KEY ("contract_id") REFERENCES "public"."completion_contracts"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_result_id_native_run_results_id_fk" FOREIGN KEY ("result_id") REFERENCES "public"."native_run_results"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_trigger_actor_company_id_companies_id_fk" FOREIGN KEY ("trigger_actor_company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "completion_contracts_issue_revision_uq" ON "completion_contracts" USING btree ("issue_id","revision");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "completion_contracts_issue_hash_uq" ON "completion_contracts" USING btree ("issue_id","canonical_sha256");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "native_run_results_run_fingerprint_uq" ON "native_run_results" USING btree ("run_id","server_fingerprint");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "native_run_results_run_caller_result_uq" ON "native_run_results" USING btree ("run_id","caller_result_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "native_run_results_run_caller_dedupe_uq" ON "native_run_results" USING btree ("run_id","caller_dedupe_key");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "status_decision_effects_decision_ordinal_uq" ON "status_decision_effects" USING btree ("decision_id","ordinal");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "status_decision_effects_company_idempotency_uq" ON "status_decision_effects" USING btree ("company_id","idempotency_key");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "status_decisions_company_issue_version_uq" ON "status_decisions" USING btree ("company_id","issue_id","decision_version");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "status_decisions_company_assessment_uq" ON "status_decisions" USING btree ("company_id","assessment_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "status_decisions_company_issue_digest_uq" ON "status_decisions" USING btree ("company_id","issue_id","decision_digest");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "work_assessments_company_issue_input_uq" ON "work_assessments" USING btree ("company_id","issue_id","input_digest");--> statement-breakpoint
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: nullable native source IDs mean historical rows need no backfill and the invariant must commit atomically with the new columns.
CREATE UNIQUE INDEX IF NOT EXISTS "heartbeat_run_events_run_source_event_uq" ON "heartbeat_run_events" USING btree ("run_id","source_event_id") WHERE "heartbeat_run_events"."source_event_id" is not null;--> statement-breakpoint
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: nullable native source sequence fields mean historical rows need no backfill and the invariant must commit atomically with the new columns.
CREATE UNIQUE INDEX IF NOT EXISTS "heartbeat_run_events_run_source_seq_uq" ON "heartbeat_run_events" USING btree ("run_id","source_instance_id","source_seq") WHERE "heartbeat_run_events"."source_instance_id" is not null and "heartbeat_run_events"."source_seq" is not null;
--> statement-breakpoint
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: the primary key already makes this ownership tuple unique; this supporting index only enables composite foreign keys.
CREATE UNIQUE INDEX IF NOT EXISTS "issues_company_id_uq" ON "issues" USING btree ("company_id","id");--> statement-breakpoint
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: the primary key already makes this ownership tuple unique; this supporting index only enables composite foreign keys.
CREATE UNIQUE INDEX IF NOT EXISTS "heartbeat_runs_company_native_issue_id_uq" ON "heartbeat_runs" USING btree ("company_id","native_issue_id","id");--> statement-breakpoint
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: the primary key already makes this ownership tuple unique; this supporting index only enables composite foreign keys.
CREATE UNIQUE INDEX IF NOT EXISTS "heartbeat_runs_company_native_issue_contract_id_uq" ON "heartbeat_runs" USING btree ("company_id","native_issue_id","id","completion_contract_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "completion_contracts_company_issue_id_uq" ON "completion_contracts" USING btree ("company_id","issue_id","id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "native_run_results_company_issue_run_id_uq" ON "native_run_results" USING btree ("company_id","issue_id","run_id","id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "work_assessments_company_issue_run_id_uq" ON "work_assessments" USING btree ("company_id","issue_id","run_id","id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "status_decisions_company_issue_id_uq" ON "status_decisions" USING btree ("company_id","issue_id","id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "status_decisions_company_issue_run_assessment_id_uq" ON "status_decisions" USING btree ("company_id","issue_id","run_id","assessment_id","id");--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "completion_contracts" ADD CONSTRAINT "completion_contracts_issue_company_fk" FOREIGN KEY ("company_id","issue_id") REFERENCES "public"."issues"("company_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "completion_contracts" ADD CONSTRAINT "completion_contracts_supersedes_owner_fk" FOREIGN KEY ("company_id","issue_id","supersedes_contract_id") REFERENCES "public"."completion_contracts"("company_id","issue_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_results" ADD CONSTRAINT "native_run_results_issue_company_fk" FOREIGN KEY ("company_id","issue_id") REFERENCES "public"."issues"("company_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_results" ADD CONSTRAINT "native_run_results_run_contract_owner_fk" FOREIGN KEY ("company_id","issue_id","run_id","completion_contract_id") REFERENCES "public"."heartbeat_runs"("company_id","native_issue_id","id","completion_contract_id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_results" ADD CONSTRAINT "native_run_results_completion_contract_owner_fk" FOREIGN KEY ("company_id","issue_id","completion_contract_id") REFERENCES "public"."completion_contracts"("company_id","issue_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_issue_company_fk" FOREIGN KEY ("company_id","issue_id") REFERENCES "public"."issues"("company_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_run_owner_fk" FOREIGN KEY ("company_id","issue_id","run_id") REFERENCES "public"."heartbeat_runs"("company_id","native_issue_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_contract_owner_fk" FOREIGN KEY ("company_id","issue_id","contract_id") REFERENCES "public"."completion_contracts"("company_id","issue_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_result_owner_fk" FOREIGN KEY ("company_id","issue_id","run_id","result_id") REFERENCES "public"."native_run_results"("company_id","issue_id","run_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_supersedes_owner_fk" FOREIGN KEY ("company_id","issue_id","run_id","supersedes_assessment_id") REFERENCES "public"."work_assessments"("company_id","issue_id","run_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "status_decisions" ADD CONSTRAINT "status_decisions_issue_company_fk" FOREIGN KEY ("company_id","issue_id") REFERENCES "public"."issues"("company_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "status_decisions" ADD CONSTRAINT "status_decisions_assessment_owner_fk" FOREIGN KEY ("company_id","issue_id","run_id","assessment_id") REFERENCES "public"."work_assessments"("company_id","issue_id","run_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "status_decisions" ADD CONSTRAINT "status_decisions_supersedes_owner_fk" FOREIGN KEY ("company_id","issue_id","supersedes_decision_id") REFERENCES "public"."status_decisions"("company_id","issue_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "status_decision_effects" ADD CONSTRAINT "status_decision_effects_issue_company_fk" FOREIGN KEY ("company_id","issue_id") REFERENCES "public"."issues"("company_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "status_decision_effects" ADD CONSTRAINT "status_decision_effects_decision_owner_fk" FOREIGN KEY ("company_id","issue_id","decision_id") REFERENCES "public"."status_decisions"("company_id","issue_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_issue_company_fk" FOREIGN KEY ("company_id","issue_id") REFERENCES "public"."issues"("company_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_run_owner_fk" FOREIGN KEY ("company_id","issue_id","run_id") REFERENCES "public"."heartbeat_runs"("company_id","native_issue_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_result_owner_fk" FOREIGN KEY ("company_id","issue_id","run_id","result_id") REFERENCES "public"."native_run_results"("company_id","issue_id","run_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_assessment_owner_fk" FOREIGN KEY ("company_id","issue_id","run_id","assessment_id") REFERENCES "public"."work_assessments"("company_id","issue_id","run_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_decision_owner_fk" FOREIGN KEY ("company_id","issue_id","run_id","assessment_id","decision_id") REFERENCES "public"."status_decisions"("company_id","issue_id","run_id","assessment_id","id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_assessment_requires_result_check" CHECK ("assessment_id" is null or "result_id" is not null);
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "native_run_finalizations" ADD CONSTRAINT "native_run_finalizations_decision_requires_assessment_check" CHECK ("decision_id" is null or "assessment_id" is not null);
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "work_assessments" ADD CONSTRAINT "work_assessments_trigger_actor_company_check" CHECK ("trigger_actor_company_id" = "company_id");
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;

File diff suppressed because it is too large Load Diff

View File

@ -1576,6 +1576,13 @@
"when": 1787261199301,
"tag": "0226_tan_colossus",
"breakpoints": true
},
{
"idx": 227,
"version": "7",
"when": 1787668790923,
"tag": "0227_modern_pandemic",
"breakpoints": true
}
]
}

View File

@ -0,0 +1,59 @@
import {
foreignKey,
pgTable,
uuid,
text,
timestamp,
integer,
jsonb,
unique,
uniqueIndex,
} from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
export const completionContracts = pgTable(
"completion_contracts",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
issueId: uuid("issue_id").notNull(),
revision: integer("revision").notNull(),
schemaVersion: text("schema_version").notNull(),
policyVersion: text("policy_version").notNull(),
risk: text("risk").notNull(),
completionAuthority: text("completion_authority").notNull(),
incompleteCriteriaPolicy: text("incomplete_criteria_policy").notNull(),
contractJson: jsonb("contract_json").$type<Record<string, unknown>>().notNull(),
canonicalSha256: text("canonical_sha256").notNull(),
createdByActorType: text("created_by_actor_type").notNull(),
createdByActorId: text("created_by_actor_id").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
supersedesContractId: uuid("supersedes_contract_id"),
},
(table) => ({
companyIssueIdUq: unique("completion_contracts_company_issue_id_uq").on(
table.companyId,
table.issueId,
table.id,
),
issueCompanyFk: foreignKey({
columns: [table.companyId, table.issueId],
foreignColumns: [issues.companyId, issues.id],
name: "completion_contracts_issue_company_fk",
}),
supersedesOwnerFk: foreignKey({
columns: [table.companyId, table.issueId, table.supersedesContractId],
foreignColumns: [table.companyId, table.issueId, table.id],
name: "completion_contracts_supersedes_owner_fk",
}),
issueRevisionUq: uniqueIndex("completion_contracts_issue_revision_uq").on(
table.issueId,
table.revision,
),
issueHashUq: uniqueIndex("completion_contracts_issue_hash_uq").on(
table.issueId,
table.canonicalSha256,
),
}),
);

View File

@ -1,4 +1,16 @@
import { pgTable, uuid, text, timestamp, integer, jsonb, index, bigserial } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
import {
pgTable,
uuid,
text,
timestamp,
integer,
jsonb,
index,
bigserial,
bigint,
uniqueIndex,
} from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { agents } from "./agents.js";
import { heartbeatRuns } from "./heartbeat_runs.js";
@ -10,19 +22,29 @@ export const heartbeatRunEvents = pgTable(
companyId: uuid("company_id").notNull().references(() => companies.id),
runId: uuid("run_id").notNull().references(() => heartbeatRuns.id),
agentId: uuid("agent_id").notNull().references(() => agents.id),
seq: integer("seq").notNull(),
seq: bigint("seq", { mode: "number" }).notNull(),
eventType: text("event_type").notNull(),
stream: text("stream"),
level: text("level"),
color: text("color"),
message: text("message"),
payload: jsonb("payload").$type<Record<string, unknown>>(),
sourceInstanceId: text("source_instance_id"),
sourceEventId: text("source_event_id"),
sourceSeq: bigint("source_seq", { mode: "number" }),
sourcePayloadSha256: text("source_payload_sha256"),
protocolSchemaVersion: integer("protocol_schema_version"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
runSeqIdx: index("heartbeat_run_events_run_seq_idx").on(table.runId, table.seq),
runSourceEventUq: uniqueIndex("heartbeat_run_events_run_source_event_uq")
.on(table.runId, table.sourceEventId)
.where(sql`${table.sourceEventId} is not null`),
runSourceSeqUq: uniqueIndex("heartbeat_run_events_run_source_seq_uq")
.on(table.runId, table.sourceInstanceId, table.sourceSeq)
.where(sql`${table.sourceInstanceId} is not null and ${table.sourceSeq} is not null`),
companyRunIdx: index("heartbeat_run_events_company_run_idx").on(table.companyId, table.runId),
companyCreatedIdx: index("heartbeat_run_events_company_created_idx").on(table.companyId, table.createdAt),
}),
);

View File

@ -1,5 +1,17 @@
import { sql } from "drizzle-orm";
import { type AnyPgColumn, pgTable, uuid, text, timestamp, jsonb, index, integer, bigint, boolean } from "drizzle-orm/pg-core";
import {
type AnyPgColumn,
pgTable,
uuid,
text,
timestamp,
jsonb,
index,
integer,
bigint,
boolean,
unique,
} from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { agents } from "./agents.js";
import { agentWakeupRequests } from "./agent_wakeup_requests.js";
@ -22,6 +34,21 @@ export const heartbeatRuns = pgTable(
signal: text("signal"),
usageJson: jsonb("usage_json").$type<Record<string, unknown>>(),
resultJson: jsonb("result_json").$type<Record<string, unknown>>(),
runtimeMode: text("runtime_mode").notNull().default("legacy"),
runtimeModeResolverVersion: text("runtime_mode_resolver_version"),
runtimeModeReason: text("runtime_mode_reason"),
runtimeModeResolvedAt: timestamp("runtime_mode_resolved_at", { withTimezone: true }),
runnerProfileJson: jsonb("runner_profile_json").$type<Record<string, unknown>>(),
runnerInstanceId: uuid("runner_instance_id"),
nativeSessionId: uuid("native_session_id"),
nativeIssueId: uuid("native_issue_id"),
driverKind: text("driver_kind"),
driverVersion: text("driver_version"),
completionContractId: uuid("completion_contract_id"),
completionContractSha256: text("completion_contract_sha256"),
nextEventSeq: bigint("next_event_seq", { mode: "number" }).notNull().default(1),
nativePhase: text("native_phase"),
nativePhaseUpdatedAt: timestamp("native_phase_updated_at", { withTimezone: true }),
sessionIdBefore: text("session_id_before"),
sessionIdAfter: text("session_id_after"),
logStore: text("log_store"),
@ -60,6 +87,19 @@ export const heartbeatRuns = pgTable(
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyNativeIssueRunUq: unique("heartbeat_runs_company_native_issue_id_uq").on(
table.companyId,
table.nativeIssueId,
table.id,
),
companyNativeIssueRunContractUq: unique(
"heartbeat_runs_company_native_issue_contract_id_uq",
).on(
table.companyId,
table.nativeIssueId,
table.id,
table.completionContractId,
),
companyAgentStartedIdx: index("heartbeat_runs_company_agent_started_idx").on(
table.companyId,
table.agentId,

View File

@ -111,6 +111,12 @@ export { documentAnnotationComments } from "./document_annotation_comments.js";
export { documentAnnotationAnchorSnapshots } from "./document_annotation_anchor_snapshots.js";
export { heartbeatRuns } from "./heartbeat_runs.js";
export { heartbeatRunEvents } from "./heartbeat_run_events.js";
export { completionContracts } from "./completion_contracts.js";
export { nativeRunResults } from "./native_run_results.js";
export { nativeRunFinalizations } from "./native_run_finalizations.js";
export { workAssessments } from "./work_assessments.js";
export { statusDecisions } from "./status_decisions.js";
export { statusDecisionEffects } from "./status_decision_effects.js";
export { heartbeatRunWatchdogDecisions } from "./heartbeat_run_watchdog_decisions.js";
export { smokeRuns, smokeRunSteps } from "./smoke_lab.js";
export { costEvents } from "./cost_events.js";

View File

@ -9,6 +9,8 @@ import {
jsonb,
index,
uniqueIndex,
unique,
bigint,
} from "drizzle-orm/pg-core";
import { agents } from "./agents.js";
import { projects } from "./projects.js";
@ -31,6 +33,8 @@ export const issues = pgTable(
title: text("title").notNull(),
description: text("description"),
status: text("status").notNull().default("backlog"),
statusVersion: bigint("status_version", { mode: "number" }).notNull().default(0),
lastStatusDecisionId: uuid("last_status_decision_id"),
workMode: text("work_mode").notNull().default("standard"),
harnessKind: text("harness_kind"),
priority: text("priority").notNull().default("medium"),
@ -77,6 +81,7 @@ export const issues = pgTable(
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyIdUq: unique("issues_company_id_uq").on(table.companyId, table.id),
companyStatusIdx: index("issues_company_status_idx").on(table.companyId, table.status),
companyHarnessKindIdx: index("issues_company_harness_kind_idx").on(table.companyId, table.harnessKind),
assigneeStatusIdx: index("issues_company_assignee_status_idx").on(

View File

@ -0,0 +1,95 @@
import { sql } from "drizzle-orm";
import {
check,
foreignKey,
pgTable,
uuid,
text,
timestamp,
integer,
jsonb,
} from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
import { heartbeatRuns } from "./heartbeat_runs.js";
import { nativeRunResults } from "./native_run_results.js";
import { statusDecisions } from "./status_decisions.js";
import { workAssessments } from "./work_assessments.js";
export const nativeRunFinalizations = pgTable(
"native_run_finalizations",
{
runId: uuid("run_id").primaryKey(),
companyId: uuid("company_id").notNull().references(() => companies.id),
issueId: uuid("issue_id").notNull(),
phase: text("phase").notNull(),
attempt: integer("attempt").notNull().default(0),
leaseOwner: text("lease_owner"),
leaseExpiresAt: timestamp("lease_expires_at", { withTimezone: true }),
resultId: uuid("result_id"),
assessmentId: uuid("assessment_id"),
decisionId: uuid("decision_id"),
failureCode: text("failure_code"),
failureDetail: jsonb("failure_detail").$type<Record<string, unknown>>(),
nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
issueCompanyFk: foreignKey({
columns: [table.companyId, table.issueId],
foreignColumns: [issues.companyId, issues.id],
name: "native_run_finalizations_issue_company_fk",
}),
runOwnerFk: foreignKey({
columns: [table.companyId, table.issueId, table.runId],
foreignColumns: [heartbeatRuns.companyId, heartbeatRuns.nativeIssueId, heartbeatRuns.id],
name: "native_run_finalizations_run_owner_fk",
}),
resultOwnerFk: foreignKey({
columns: [table.companyId, table.issueId, table.runId, table.resultId],
foreignColumns: [
nativeRunResults.companyId,
nativeRunResults.issueId,
nativeRunResults.runId,
nativeRunResults.id,
],
name: "native_run_finalizations_result_owner_fk",
}),
assessmentOwnerFk: foreignKey({
columns: [table.companyId, table.issueId, table.runId, table.assessmentId],
foreignColumns: [
workAssessments.companyId,
workAssessments.issueId,
workAssessments.runId,
workAssessments.id,
],
name: "native_run_finalizations_assessment_owner_fk",
}),
decisionOwnerFk: foreignKey({
columns: [
table.companyId,
table.issueId,
table.runId,
table.assessmentId,
table.decisionId,
],
foreignColumns: [
statusDecisions.companyId,
statusDecisions.issueId,
statusDecisions.runId,
statusDecisions.assessmentId,
statusDecisions.id,
],
name: "native_run_finalizations_decision_owner_fk",
}),
assessmentRequiresResultCheck: check(
"native_run_finalizations_assessment_requires_result_check",
sql`${table.assessmentId} is null or ${table.resultId} is not null`,
),
decisionRequiresAssessmentCheck: check(
"native_run_finalizations_decision_requires_assessment_check",
sql`${table.decisionId} is null or ${table.assessmentId} is not null`,
),
}),
);

View File

@ -0,0 +1,83 @@
import {
foreignKey,
pgTable,
uuid,
text,
timestamp,
jsonb,
unique,
uniqueIndex,
} from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
import { heartbeatRuns } from "./heartbeat_runs.js";
import { completionContracts } from "./completion_contracts.js";
export const nativeRunResults = pgTable(
"native_run_results",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
issueId: uuid("issue_id").notNull(),
runId: uuid("run_id").notNull(),
turnId: text("turn_id"),
completionContractId: uuid("completion_contract_id").notNull(),
callerResultId: text("caller_result_id"),
callerDedupeKey: text("caller_dedupe_key"),
serverFingerprint: text("server_fingerprint").notNull(),
schemaStatus: text("schema_status").notNull(),
rejectionCode: text("rejection_code"),
resultJson: jsonb("result_json").$type<Record<string, unknown>>().notNull(),
canonicalSha256: text("canonical_sha256").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyIssueRunIdUq: unique("native_run_results_company_issue_run_id_uq").on(
table.companyId,
table.issueId,
table.runId,
table.id,
),
issueCompanyFk: foreignKey({
columns: [table.companyId, table.issueId],
foreignColumns: [issues.companyId, issues.id],
name: "native_run_results_issue_company_fk",
}),
runContractOwnerFk: foreignKey({
columns: [
table.companyId,
table.issueId,
table.runId,
table.completionContractId,
],
foreignColumns: [
heartbeatRuns.companyId,
heartbeatRuns.nativeIssueId,
heartbeatRuns.id,
heartbeatRuns.completionContractId,
],
name: "native_run_results_run_contract_owner_fk",
}),
completionContractOwnerFk: foreignKey({
columns: [table.companyId, table.issueId, table.completionContractId],
foreignColumns: [
completionContracts.companyId,
completionContracts.issueId,
completionContracts.id,
],
name: "native_run_results_completion_contract_owner_fk",
}),
runFingerprintUq: uniqueIndex("native_run_results_run_fingerprint_uq").on(
table.runId,
table.serverFingerprint,
),
runCallerResultUq: uniqueIndex("native_run_results_run_caller_result_uq").on(
table.runId,
table.callerResultId,
),
runCallerDedupeUq: uniqueIndex("native_run_results_run_caller_dedupe_uq").on(
table.runId,
table.callerDedupeKey,
),
}),
);

View File

@ -0,0 +1,56 @@
import {
foreignKey,
pgTable,
uuid,
text,
timestamp,
integer,
jsonb,
uniqueIndex,
} from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
import { statusDecisions } from "./status_decisions.js";
export const statusDecisionEffects = pgTable(
"status_decision_effects",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
issueId: uuid("issue_id").notNull(),
decisionId: uuid("decision_id").notNull(),
ordinal: integer("ordinal").notNull(),
effectKind: text("effect_kind").notNull(),
targetType: text("target_type").notNull(),
targetId: text("target_id"),
idempotencyKey: text("idempotency_key").notNull(),
payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
deliveryState: text("delivery_state").notNull().default("pending"),
attemptCount: integer("attempt_count").notNull().default(0),
nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }),
lastError: text("last_error"),
deliveredAt: timestamp("delivered_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
issueCompanyFk: foreignKey({
columns: [table.companyId, table.issueId],
foreignColumns: [issues.companyId, issues.id],
name: "status_decision_effects_issue_company_fk",
}),
decisionOwnerFk: foreignKey({
columns: [table.companyId, table.issueId, table.decisionId],
foreignColumns: [statusDecisions.companyId, statusDecisions.issueId, statusDecisions.id],
name: "status_decision_effects_decision_owner_fk",
}),
decisionOrdinalUq: uniqueIndex("status_decision_effects_decision_ordinal_uq").on(
table.decisionId,
table.ordinal,
),
idempotencyUq: uniqueIndex("status_decision_effects_company_idempotency_uq").on(
table.companyId,
table.idempotencyKey,
),
}),
);

View File

@ -0,0 +1,86 @@
import {
foreignKey,
pgTable,
uuid,
text,
timestamp,
bigint,
jsonb,
unique,
uniqueIndex,
} from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
import { workAssessments } from "./work_assessments.js";
export const statusDecisions = pgTable(
"status_decisions",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
issueId: uuid("issue_id").notNull(),
runId: uuid("run_id").notNull(),
assessmentId: uuid("assessment_id").notNull(),
decisionVersion: bigint("decision_version", { mode: "number" }).notNull(),
policyVersion: text("policy_version").notNull(),
fromStatus: text("from_status").notNull(),
toStatus: text("to_status").notNull(),
reasonCode: text("reason_code").notNull(),
decisionJson: jsonb("decision_json").$type<Record<string, unknown>>().notNull(),
decisionDigest: text("decision_digest").notNull(),
applicationState: text("application_state").notNull().default("proposed"),
supersedesDecisionId: uuid("supersedes_decision_id"),
appliedAt: timestamp("applied_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyIssueIdUq: unique("status_decisions_company_issue_id_uq").on(
table.companyId,
table.issueId,
table.id,
),
companyIssueRunAssessmentIdUq: unique(
"status_decisions_company_issue_run_assessment_id_uq",
).on(
table.companyId,
table.issueId,
table.runId,
table.assessmentId,
table.id,
),
issueCompanyFk: foreignKey({
columns: [table.companyId, table.issueId],
foreignColumns: [issues.companyId, issues.id],
name: "status_decisions_issue_company_fk",
}),
assessmentOwnerFk: foreignKey({
columns: [table.companyId, table.issueId, table.runId, table.assessmentId],
foreignColumns: [
workAssessments.companyId,
workAssessments.issueId,
workAssessments.runId,
workAssessments.id,
],
name: "status_decisions_assessment_owner_fk",
}),
supersedesOwnerFk: foreignKey({
columns: [table.companyId, table.issueId, table.supersedesDecisionId],
foreignColumns: [table.companyId, table.issueId, table.id],
name: "status_decisions_supersedes_owner_fk",
}),
issueVersionUq: uniqueIndex("status_decisions_company_issue_version_uq").on(
table.companyId,
table.issueId,
table.decisionVersion,
),
assessmentUq: uniqueIndex("status_decisions_company_assessment_uq").on(
table.companyId,
table.assessmentId,
),
issueDigestUq: uniqueIndex("status_decisions_company_issue_digest_uq").on(
table.companyId,
table.issueId,
table.decisionDigest,
),
}),
);

View File

@ -0,0 +1,94 @@
import { sql } from "drizzle-orm";
import {
check,
foreignKey,
pgTable,
uuid,
text,
timestamp,
bigint,
jsonb,
unique,
uniqueIndex,
} from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
import { heartbeatRuns } from "./heartbeat_runs.js";
import { completionContracts } from "./completion_contracts.js";
import { nativeRunResults } from "./native_run_results.js";
export const workAssessments = pgTable(
"work_assessments",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id),
issueId: uuid("issue_id").notNull(),
runId: uuid("run_id").notNull(),
turnId: text("turn_id"),
contractId: uuid("contract_id").notNull(),
resultId: uuid("result_id").notNull(),
triggerKind: text("trigger_kind").notNull(),
triggerRef: text("trigger_ref"),
triggerCapability: text("trigger_capability"),
triggerActorCompanyId: uuid("trigger_actor_company_id").notNull().references(() => companies.id),
priorIssueStatus: text("prior_issue_status").notNull(),
priorStatusVersion: bigint("prior_status_version", { mode: "number" }).notNull(),
priorDecisionId: uuid("prior_decision_id"),
policyVersion: text("policy_version").notNull(),
assessmentJson: jsonb("assessment_json").$type<Record<string, unknown>>().notNull(),
inputDigest: text("input_digest").notNull(),
supersedesAssessmentId: uuid("supersedes_assessment_id"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
companyIssueRunIdUq: unique("work_assessments_company_issue_run_id_uq").on(
table.companyId,
table.issueId,
table.runId,
table.id,
),
issueCompanyFk: foreignKey({
columns: [table.companyId, table.issueId],
foreignColumns: [issues.companyId, issues.id],
name: "work_assessments_issue_company_fk",
}),
runOwnerFk: foreignKey({
columns: [table.companyId, table.issueId, table.runId],
foreignColumns: [heartbeatRuns.companyId, heartbeatRuns.nativeIssueId, heartbeatRuns.id],
name: "work_assessments_run_owner_fk",
}),
contractOwnerFk: foreignKey({
columns: [table.companyId, table.issueId, table.contractId],
foreignColumns: [
completionContracts.companyId,
completionContracts.issueId,
completionContracts.id,
],
name: "work_assessments_contract_owner_fk",
}),
resultOwnerFk: foreignKey({
columns: [table.companyId, table.issueId, table.runId, table.resultId],
foreignColumns: [
nativeRunResults.companyId,
nativeRunResults.issueId,
nativeRunResults.runId,
nativeRunResults.id,
],
name: "work_assessments_result_owner_fk",
}),
supersedesOwnerFk: foreignKey({
columns: [table.companyId, table.issueId, table.runId, table.supersedesAssessmentId],
foreignColumns: [table.companyId, table.issueId, table.runId, table.id],
name: "work_assessments_supersedes_owner_fk",
}),
triggerActorCompanyCheck: check(
"work_assessments_trigger_actor_company_check",
sql`${table.triggerActorCompanyId} = ${table.companyId}`,
),
issueInputUq: uniqueIndex("work_assessments_company_issue_input_uq").on(
table.companyId,
table.issueId,
table.inputDigest,
),
}),
);

View File

@ -1,5 +1,19 @@
export { agentAdapterTypeSchema, optionalAgentAdapterTypeSchema } from "./adapter-type.js";
export { ADAPTER_AUTH_MISSING_CHECK_CODE } from "./adapter-auth-check-code.js";
export {
nativeFinalizationResultSchema,
nativeFinalizationResultV1Schema,
nativeReportedWorkDispositionSchema,
type NativeFinalizationResultInput,
} from "./validators/native-finalization.js";
export {
NATIVE_FINALIZATION_SCHEMA,
type NativeFinalizationResult,
type NativeFinalizationResultV1,
type NativeReportedWorkDisposition,
type NativeRuntimeMode,
type NativeRunTerminalState,
} from "./types/native-finalization.js";
export {
decisionEffectStalenessSchema,
decisionOptionStyleSchema,

View File

@ -1,4 +1,12 @@
export { decisionEffectTargetIssueIds } from "./decision.js";
export {
NATIVE_FINALIZATION_SCHEMA,
type NativeFinalizationResult,
type NativeFinalizationResultV1,
type NativeReportedWorkDisposition,
type NativeRuntimeMode,
type NativeRunTerminalState,
} from "./native-finalization.js";
export type {
Company,
InteractionResolverGovernance,

View File

@ -0,0 +1,30 @@
export type NativeRuntimeMode = "legacy" | "native";
export const NATIVE_FINALIZATION_SCHEMA = "paperclip.native-finalization.v1" as const;
export type NativeReportedWorkDisposition = "done" | "blocked" | "needs_review" | "yielded";
export type NativeRunTerminalState = "succeeded" | "failed" | "cancelled";
export interface NativeFinalizationResultV1 {
schema: typeof NATIVE_FINALIZATION_SCHEMA;
runtimeMode: "native";
runId: string;
issueId: string;
companyId: string;
result: Record<string, unknown>;
terminal: {
schema: "paperclip.prp.terminal.v1";
turnTerminalState: "completed" | "failed" | "interrupted" | "cancelled";
runTerminalState: NativeRunTerminalState;
reportedWorkDisposition: NativeReportedWorkDisposition;
};
turnId: string | null;
sourceInstanceId: string;
normalizedSessionId: string;
providerSessionId: string | null;
driverKind: string;
driverVersion: string;
nativeEventCount: number;
highestContiguousSourceSeq: number;
workspaceFinalizeStatus: "pending" | "succeeded" | "failed";
}
export type NativeFinalizationResult = NativeFinalizationResultV1;

View File

@ -1,3 +1,10 @@
export {
nativeFinalizationResultSchema,
nativeFinalizationResultV1Schema,
nativeReportedWorkDispositionSchema,
type NativeFinalizationResultInput,
} from "./native-finalization.js";
export {
decisionEffectStalenessSchema,
decisionOptionStyleSchema,

View File

@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { NATIVE_FINALIZATION_SCHEMA } from "../types/native-finalization.js";
import { nativeFinalizationResultSchema } from "./native-finalization.js";
const validResult = {
schema: NATIVE_FINALIZATION_SCHEMA,
runtimeMode: "native",
runId: "10000000-0000-4000-8000-000000000001",
issueId: "10000000-0000-4000-8000-000000000002",
companyId: "10000000-0000-4000-8000-000000000003",
result: { summary: "Completed the requested work" },
terminal: {
schema: "paperclip.prp.terminal.v1",
turnTerminalState: "completed",
runTerminalState: "succeeded",
reportedWorkDisposition: "done",
},
turnId: "turn-1",
sourceInstanceId: "runner-instance-1",
normalizedSessionId: "session-1",
providerSessionId: "provider-session-1",
driverKind: "codex",
driverVersion: "1.0.0",
nativeEventCount: 12,
highestContiguousSourceSeq: 12,
workspaceFinalizeStatus: "succeeded",
} as const;
describe("native finalization validators", () => {
it("accepts the complete v1 finalization contract", () => {
expect(nativeFinalizationResultSchema.parse(validResult)).toEqual(validResult);
});
it("fails closed for unknown required versions and extra fields", () => {
expect(nativeFinalizationResultSchema.safeParse({
...validResult,
schema: "paperclip.native-finalization.v2",
}).success).toBe(false);
expect(nativeFinalizationResultSchema.safeParse({
...validResult,
unrecognizedRequiredField: true,
}).success).toBe(false);
});
it("rejects invalid identities and sequence counters", () => {
expect(nativeFinalizationResultSchema.safeParse({
...validResult,
runId: "not-a-uuid",
}).success).toBe(false);
expect(nativeFinalizationResultSchema.safeParse({
...validResult,
highestContiguousSourceSeq: -1,
}).success).toBe(false);
});
});

View File

@ -0,0 +1,35 @@
import { z } from "zod";
import { NATIVE_FINALIZATION_SCHEMA } from "../types/native-finalization.js";
export const nativeFinalizationResultV1Schema = z.object({
schema: z.literal(NATIVE_FINALIZATION_SCHEMA),
runtimeMode: z.literal("native"),
runId: z.string().uuid(),
issueId: z.string().uuid(),
companyId: z.string().uuid(),
result: z.record(z.string(), z.unknown()),
terminal: z.object({
schema: z.literal("paperclip.prp.terminal.v1"),
turnTerminalState: z.enum(["completed", "failed", "interrupted", "cancelled"]),
runTerminalState: z.enum(["succeeded", "failed", "cancelled"]),
reportedWorkDisposition: z.enum(["done", "blocked", "needs_review", "yielded"]),
}).strict(),
turnId: z.string().min(1).nullable(),
sourceInstanceId: z.string().min(1).max(160),
normalizedSessionId: z.string().min(1),
providerSessionId: z.string().min(1).nullable(),
driverKind: z.string().min(1),
driverVersion: z.string().min(1),
nativeEventCount: z.number().int().nonnegative(),
highestContiguousSourceSeq: z.number().int().nonnegative(),
workspaceFinalizeStatus: z.enum(["pending", "succeeded", "failed"]),
}).strict();
export const nativeReportedWorkDispositionSchema = z.enum([
"done",
"blocked",
"needs_review",
"yielded",
]);
export const nativeFinalizationResultSchema = nativeFinalizationResultV1Schema;
export type NativeFinalizationResultInput = z.infer<typeof nativeFinalizationResultSchema>;

View File

@ -3203,6 +3203,8 @@ const issueListSelect = {
END
`,
status: issues.status,
statusVersion: issues.statusVersion,
lastStatusDecisionId: issues.lastStatusDecisionId,
workMode: issues.workMode,
harnessKind: issues.harnessKind,
priority: issues.priority,