feat: add delegation ledger infrastructure#314
Conversation
…estion and trust reports Introduce a first-class delegation ledger that unifies how tasks pass between agents and how trust is earned from evidence rather than prose. - schema: DelegationEvent (append-only lifecycle/AOP-Sonder log), DelegationValidation (Lattice StateContract + validation evidence), DelegationReceipt (Receipts proof-of-work + integrity status) - migration incl. RLS policies + engram_app grants - DelegationLedgerService: event recording, Lattice validation ingestion, receipt attach w/ SHA-256 payload integrity check, evidence-based trust signals, per-task and per-agent trust reports - lifecycle hooks: contract/task create+status changes emit events - API: /v1/delegation/events, /contracts/:id/validations, /tasks/:id/receipts, /tasks/:id/trust-report, /trust-reports/:agentId - tests: 8 suites / 57 tests (ledger, controller, lifecycle, trust rules)
|
Caution Review failedFailed to post review comments. We encountered an issue with GitHub. Use ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (14)
⏰ Context from checks skipped due to timeout. (4)
🧰 Additional context used📓 Path-based instructions (5)src/**/*.dto.ts📄 CodeRabbit inference engine (CLAUDE.md)
Files:
prisma/migrations/**/*.sql📄 CodeRabbit inference engine (CLAUDE.md)
Files:
src/**/*.spec.ts📄 CodeRabbit inference engine (CLAUDE.md)
Files:
src/**/*.controller.ts📄 CodeRabbit inference engine (CLAUDE.md)
Files:
src/**/*.service.ts📄 CodeRabbit inference engine (CLAUDE.md)
Files:
📝 WalkthroughWalkthroughThis PR introduces a "Delegation Ledger" subsystem: a new database schema (migration + Prisma models) for append-only events, validations, and receipts tied to delegation contracts/tasks, plus a ChangesDelegation Ledger
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant TaskService
participant Prisma as PrismaService
participant Ledger as DelegationLedgerService
Client->>TaskService: update(taskId, dto)
TaskService->>Prisma: update delegated task
Prisma-->>TaskService: updated task
alt dto.status provided
TaskService->>Ledger: recordEvent(eventTypeForStatus(status))
Ledger->>Prisma: create delegationEvent
end
TaskService-->>Client: updated task
sequenceDiagram
participant Client
participant Controller as DelegationLedgerController
participant Ledger as DelegationLedgerService
participant Prisma as PrismaService
Client->>Controller: POST /contracts/:id/validations
Controller->>Ledger: recordValidation(userId, contractId, dto)
Ledger->>Prisma: assertContractExists / assertTaskExists
Ledger->>Prisma: create delegationValidation
Ledger->>Prisma: create delegationEvent (HANDOFF_VALIDATED)
Ledger->>Ledger: recordTrustFromValidation
Ledger->>Prisma: create trustSignal
Ledger-->>Controller: validation result
Controller-->>Client: response
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ Recall Benchmark ResultsFull outputCommit: 16a3b5b |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (6)
src/delegation/dto/record-event.dto.ts (1)
3-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider deriving values from Prisma-generated enums instead of hand-duplicating.
DELEGATION_EVENT_TYPES/DELEGATION_EVENT_SOURCESduplicateDelegationEventType/DelegationEventSourcefromschema.prisma. Three independent lists (SQL enum, Prisma enum, this TS const) can drift as new event types are added.-import { IsIn, IsObject, IsOptional, IsString } from 'class-validator'; - -export const DELEGATION_EVENT_TYPES = [ - 'CONTRACT_CREATED', - ... -] as const; - -export const DELEGATION_EVENT_SOURCES = [ - 'ENGRAM', - ... -] as const; - -export type DelegationEventType = (typeof DELEGATION_EVENT_TYPES)[number]; -export type DelegationEventSource = (typeof DELEGATION_EVENT_SOURCES)[number]; +import { IsIn, IsObject, IsOptional, IsString } from 'class-validator'; +import { + DelegationEventType, + DelegationEventSource, +} from '`@prisma/client`'; + +export const DELEGATION_EVENT_TYPES = Object.values(DelegationEventType); +export const DELEGATION_EVENT_SOURCES = Object.values(DelegationEventSource); +export { DelegationEventType, DelegationEventSource };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/delegation/dto/record-event.dto.ts` around lines 3 - 30, The delegation event type/source lists are hand-duplicated in record-event.dto and can drift from the Prisma schema enums. Update the constants and types in DELEGATION_EVENT_TYPES, DELEGATION_EVENT_SOURCES, DelegationEventType, and DelegationEventSource to derive directly from the Prisma-generated enums (or a shared source of truth) instead of maintaining separate literal arrays in this DTO. Ensure any consumers still get the same typed union behavior without duplicating the enum values here.src/delegation/task.service.spec.ts (1)
30-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider
jest.clearAllMocks()per stated testing convention.Mocks are rebuilt fresh each test rather than using
jest.clearAllMocks(), which is functionally fine here but deviates from the stated pattern.As per coding guidelines, "Mock with jest.fn() per model method and use
jest.clearAllMocks()inbeforeEach."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/delegation/task.service.spec.ts` around lines 30 - 52, The test setup in TaskService is rebuilding the Prisma and ledger mocks inside beforeEach instead of following the stated convention. Move the mock reset to jest.clearAllMocks() in the beforeEach for task.service.spec.ts, and keep the existing jest.fn() model-method mocks for PrismaService and DelegationLedgerService so TaskService tests still get fresh call history each run.Source: Coding guidelines
src/delegation/delegation-ledger.service.spec.ts (1)
43-97: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding
jest.clearAllMocks()inbeforeEach.Mocks are freshly created per test here so isolation is preserved in practice, but the coding guideline for
src/**/*.spec.tsexplicitly calls forjest.clearAllMocks()inbeforeEachalongside manual jest.fn() mocks.As per coding guidelines, "Mock with jest.fn() per model method and use
jest.clearAllMocks()inbeforeEach."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/delegation/delegation-ledger.service.spec.ts` around lines 43 - 97, Add jest.clearAllMocks() at the start of the beforeEach in DelegationLedgerService spec so the manual jest.fn() mocks on prisma delegationContract, delegatedTask, delegationEvent, delegationValidation, delegationReceipt, and trustSignal are reset per test in line with the spec guideline.Source: Coding guidelines
src/delegation/delegation-ledger.controller.spec.ts (1)
10-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding
jest.clearAllMocks()inbeforeEach.Same guideline gap as the other spec files — mocks are rebuilt per test here so isolation holds, but the mandated pattern includes
jest.clearAllMocks().As per coding guidelines, "Mock with jest.fn() per model method and use
jest.clearAllMocks()inbeforeEach."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/delegation/delegation-ledger.controller.spec.ts` around lines 10 - 30, The test setup in DelegationLedgerController spec rebuilds jest.fn() mocks per test, but it is missing the required jest.clearAllMocks() pattern. Add jest.clearAllMocks() to the beforeEach in DelegationLedgerController spec so the mocked methods on DelegationLedgerService stay isolated and consistent with the other spec files.Source: Coding guidelines
src/delegation/contract.service.spec.ts (1)
29-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding
jest.clearAllMocks()inbeforeEach.Same guideline gap noted in the other spec files.
As per coding guidelines, "Mock with jest.fn() per model method and use
jest.clearAllMocks()inbeforeEach."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/delegation/contract.service.spec.ts` around lines 29 - 52, The contract service spec is missing the required mock reset step, so add jest.clearAllMocks() in the beforeEach that sets up ContractService, PrismaService, and DelegationLedgerService. Keep the existing jest.fn() per model method on prisma.delegationContract and ledger.recordEvent, and make sure the clear call happens before creating the TestingModule so mock state does not leak between tests.Source: Coding guidelines
src/delegation/delegation-ledger.service.ts (1)
224-230: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winJSON-path trust signal lookup uses
as anyand has no supporting index.
metadata: { path: ['delegationTaskId'], equals: taskId } as anybypasses Prisma's generated types (masking potential typos/shape errors) and filters on an un-indexed JSON path.TrustSignal(per schema) only indexes[userId, createdAt],[userId, category],[userId, signalType],[agentId, createdAt]— none support this path-based lookup, so this query will scan all of a user's trust signals as data grows.Consider adding a first-class
delegationTaskIdcolumn (indexed) onTrustSignalinstead of querying nested JSON, and use the proper PrismaJsonFiltertype rather thanany.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/delegation/delegation-ledger.service.ts` around lines 224 - 230, The `findMany` query in `DelegationLedgerService` is using a JSON-path filter with `as any`, which hides type errors and is not backed by an index. Replace the nested `metadata.delegationTaskId` lookup with a first-class `delegationTaskId` field on `TrustSignal` (and index it), then update the `trustSignal.findMany` call to filter on that typed column instead of the JSON path. Also remove the `as any` cast so Prisma’s generated types validate the query shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@prisma/migrations/20260702143000_delegation_ledger/migration.sql`:
- Around line 118-131: The RLS policy setup for delegation_events,
delegation_validations, and delegation_receipts should be made idempotent by
dropping any existing account_isolation policy before recreating it. Update the
migration statements around the CREATE POLICY calls to use DROP POLICY IF EXISTS
for each table first, then CREATE POLICY account_isolation, so re-running the
migration won’t fail; apply this pattern consistently across all three tables in
the migration.
- Around line 4-116: The migration in migration.sql is not idempotent, so
rerunning it can fail on existing enums, tables, indexes, or constraints. Update
the DelegationEventType and DelegationEventSource creation to use a DO block
guard, and add existence checks around the delegation_events,
delegation_validations, and delegation_receipts table creation. Also guard every
CREATE INDEX and ALTER TABLE ADD CONSTRAINT in this migration so repeated
applies do not raise already-exists errors.
In `@prisma/schema.prisma`:
- Line 2227: The DelegationValidation.contract relation is currently using
Cascade deletion, so removing a contract deletes validation evidence rows too.
Update the DelegationValidation model to make contractId nullable and change the
contract relation to SetNull, following the same pattern used by DelegationEvent
and DelegationReceipt, and make sure any related schema references and generated
migration reflect the nullable foreign key.
In `@src/delegation/contract.service.ts`:
- Around line 39-51: Wrap the best-effort ledger audit call in ContractService
so it cannot fail the primary database write: in both the create and update
paths around this.recordEvent/ledger?.recordEvent call, catch and swallow/log
any thrown error after the contract has already been persisted. Use the
ContractService create/update methods and the ledger.recordEvent invocation as
the key locations, and keep the contract return path unchanged even if the
ledger write fails.
In `@src/delegation/delegation-ledger.service.ts`:
- Around line 86-127: The ledger write flow is not atomic because
`recordValidation` (and the same pattern in `attachReceipt`) commits multiple
Prisma operations separately. Wrap the full sequence in
`this.prisma.$transaction(async (tx) => {...})` and make `recordValidation`,
`recordEvent`, `recordTrustFromValidation`, and `recordTrustFromReceipt` use the
transaction client instead of always calling `this.prisma`. Ensure the
validation/receipt row, lifecycle event, and trust-signal writes all succeed or
fail together in `DelegationLedgerService`.
- Around line 129-210: The attachReceipt flow in DelegationLedgerService is not
idempotent beyond the delegationsReceipt upsert, because recordEvent and
recordTrustFromReceipt always run even when the same receipt is resent. Update
attachReceipt to detect whether the existing receipt row already matches the
incoming receipt content (for example by checking before
delegationsReceipt.upsert or comparing the returned row fields), and only emit
the RECEIPT_ATTACHED event and trust update when the receipt is new or changed.
Use the attachReceipt, recordEvent, and recordTrustFromReceipt paths to keep
duplicate submissions from creating duplicate events or trust signals.
In `@src/delegation/task.service.ts`:
- Around line 28-41: The awaited ledger write in TaskService after the task
create/update commit can throw and incorrectly fail an already-persisted
mutation. Wrap the `this.ledger?.recordEvent(...)` call in `TaskService.create`
and `TaskService.update` with the same `try/catch` handling, so ledger failures
are logged/ignored or otherwise isolated from the Prisma transaction result. Use
the `TaskService`, `create`, `update`, and `recordEvent` symbols to find both
call sites and keep the task mutation success path from being rejected by ledger
errors.
---
Nitpick comments:
In `@src/delegation/contract.service.spec.ts`:
- Around line 29-52: The contract service spec is missing the required mock
reset step, so add jest.clearAllMocks() in the beforeEach that sets up
ContractService, PrismaService, and DelegationLedgerService. Keep the existing
jest.fn() per model method on prisma.delegationContract and ledger.recordEvent,
and make sure the clear call happens before creating the TestingModule so mock
state does not leak between tests.
In `@src/delegation/delegation-ledger.controller.spec.ts`:
- Around line 10-30: The test setup in DelegationLedgerController spec rebuilds
jest.fn() mocks per test, but it is missing the required jest.clearAllMocks()
pattern. Add jest.clearAllMocks() to the beforeEach in
DelegationLedgerController spec so the mocked methods on DelegationLedgerService
stay isolated and consistent with the other spec files.
In `@src/delegation/delegation-ledger.service.spec.ts`:
- Around line 43-97: Add jest.clearAllMocks() at the start of the beforeEach in
DelegationLedgerService spec so the manual jest.fn() mocks on prisma
delegationContract, delegatedTask, delegationEvent, delegationValidation,
delegationReceipt, and trustSignal are reset per test in line with the spec
guideline.
In `@src/delegation/delegation-ledger.service.ts`:
- Around line 224-230: The `findMany` query in `DelegationLedgerService` is
using a JSON-path filter with `as any`, which hides type errors and is not
backed by an index. Replace the nested `metadata.delegationTaskId` lookup with a
first-class `delegationTaskId` field on `TrustSignal` (and index it), then
update the `trustSignal.findMany` call to filter on that typed column instead of
the JSON path. Also remove the `as any` cast so Prisma’s generated types
validate the query shape.
In `@src/delegation/dto/record-event.dto.ts`:
- Around line 3-30: The delegation event type/source lists are hand-duplicated
in record-event.dto and can drift from the Prisma schema enums. Update the
constants and types in DELEGATION_EVENT_TYPES, DELEGATION_EVENT_SOURCES,
DelegationEventType, and DelegationEventSource to derive directly from the
Prisma-generated enums (or a shared source of truth) instead of maintaining
separate literal arrays in this DTO. Ensure any consumers still get the same
typed union behavior without duplicating the enum values here.
In `@src/delegation/task.service.spec.ts`:
- Around line 30-52: The test setup in TaskService is rebuilding the Prisma and
ledger mocks inside beforeEach instead of following the stated convention. Move
the mock reset to jest.clearAllMocks() in the beforeEach for
task.service.spec.ts, and keep the existing jest.fn() model-method mocks for
PrismaService and DelegationLedgerService so TaskService tests still get fresh
call history each run.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 406990a0-e372-4243-ae9a-c24ca4bb92e0
📒 Files selected for processing (14)
prisma/migrations/20260702143000_delegation_ledger/migration.sqlprisma/schema.prismasrc/delegation/contract.service.spec.tssrc/delegation/contract.service.tssrc/delegation/delegation-ledger.controller.spec.tssrc/delegation/delegation-ledger.controller.tssrc/delegation/delegation-ledger.service.spec.tssrc/delegation/delegation-ledger.service.tssrc/delegation/delegation.module.tssrc/delegation/dto/attach-receipt.dto.tssrc/delegation/dto/record-event.dto.tssrc/delegation/dto/record-validation.dto.tssrc/delegation/task.service.spec.tssrc/delegation/task.service.ts
| CREATE TYPE "DelegationEventType" AS ENUM ( | ||
| 'CONTRACT_CREATED', | ||
| 'CONTRACT_ACCEPTED', | ||
| 'CONTRACT_STARTED', | ||
| 'CONTRACT_COMPLETED', | ||
| 'CONTRACT_VERIFIED', | ||
| 'CONTRACT_REJECTED', | ||
| 'TASK_ASSIGNED', | ||
| 'TASK_STARTED', | ||
| 'TASK_COMPLETED', | ||
| 'TASK_FAILED', | ||
| 'HANDOFF_VALIDATED', | ||
| 'RECEIPT_ATTACHED', | ||
| 'TRUST_SCORED', | ||
| 'CHALLENGE_RAISED', | ||
| 'AOP_EVENT_RECORDED' | ||
| ); | ||
|
|
||
| CREATE TYPE "DelegationEventSource" AS ENUM ( | ||
| 'ENGRAM', | ||
| 'SONDER', | ||
| 'LATTICE', | ||
| 'RECEIPTS', | ||
| 'OPENCLAW' | ||
| ); | ||
|
|
||
| CREATE TABLE "delegation_events" ( | ||
| "id" TEXT NOT NULL, | ||
| "user_id" TEXT NOT NULL, | ||
| "contract_id" TEXT, | ||
| "task_id" TEXT, | ||
| "agent_session_key" TEXT, | ||
| "event_type" "DelegationEventType" NOT NULL, | ||
| "agent_id" TEXT, | ||
| "parent_event_id" TEXT, | ||
| "trace_id" TEXT, | ||
| "source" "DelegationEventSource" NOT NULL DEFAULT 'ENGRAM', | ||
| "payload" JSONB NOT NULL DEFAULT '{}'::jsonb, | ||
| "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| CONSTRAINT "delegation_events_pkey" PRIMARY KEY ("id") | ||
| ); | ||
|
|
||
| CREATE TABLE "delegation_validations" ( | ||
| "id" TEXT NOT NULL, | ||
| "user_id" TEXT NOT NULL, | ||
| "contract_id" TEXT NOT NULL, | ||
| "task_id" TEXT, | ||
| "lattice_contract_id" TEXT, | ||
| "trace_id" TEXT, | ||
| "passed" BOOLEAN NOT NULL, | ||
| "tier" TEXT NOT NULL, | ||
| "tiers_run" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], | ||
| "duration_ms" INTEGER, | ||
| "reason" TEXT, | ||
| "confidence" DOUBLE PRECISION, | ||
| "provider_failure" BOOLEAN NOT NULL DEFAULT false, | ||
| "evidence" JSONB NOT NULL DEFAULT '[]'::jsonb, | ||
| "state_contract" JSONB NOT NULL, | ||
| "validation_result" JSONB NOT NULL DEFAULT '{}'::jsonb, | ||
| "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| CONSTRAINT "delegation_validations_pkey" PRIMARY KEY ("id") | ||
| ); | ||
|
|
||
| CREATE TABLE "delegation_receipts" ( | ||
| "id" TEXT NOT NULL, | ||
| "user_id" TEXT NOT NULL, | ||
| "contract_id" TEXT, | ||
| "task_id" TEXT, | ||
| "receipt_id" TEXT NOT NULL, | ||
| "status" TEXT NOT NULL, | ||
| "claim_summary" TEXT NOT NULL, | ||
| "actor_id" TEXT, | ||
| "actor_model" TEXT, | ||
| "verification_summary" TEXT, | ||
| "checks" JSONB NOT NULL DEFAULT '[]'::jsonb, | ||
| "risk_level" TEXT, | ||
| "artifact_uri" TEXT, | ||
| "payload_hash" TEXT, | ||
| "artifact_hashes" JSONB NOT NULL DEFAULT '[]'::jsonb, | ||
| "receipt" JSONB NOT NULL, | ||
| "integrity_status" TEXT, | ||
| "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| CONSTRAINT "delegation_receipts_pkey" PRIMARY KEY ("id") | ||
| ); | ||
|
|
||
| CREATE INDEX "delegation_events_user_id_created_at_idx" ON "delegation_events"("user_id", "created_at"); | ||
| CREATE INDEX "delegation_events_contract_id_created_at_idx" ON "delegation_events"("contract_id", "created_at"); | ||
| CREATE INDEX "delegation_events_task_id_created_at_idx" ON "delegation_events"("task_id", "created_at"); | ||
| CREATE INDEX "delegation_events_agent_id_created_at_idx" ON "delegation_events"("agent_id", "created_at"); | ||
| CREATE INDEX "delegation_events_trace_id_idx" ON "delegation_events"("trace_id"); | ||
| CREATE INDEX "delegation_events_event_type_idx" ON "delegation_events"("event_type"); | ||
|
|
||
| CREATE INDEX "delegation_validations_user_id_created_at_idx" ON "delegation_validations"("user_id", "created_at"); | ||
| CREATE INDEX "delegation_validations_contract_id_created_at_idx" ON "delegation_validations"("contract_id", "created_at"); | ||
| CREATE INDEX "delegation_validations_task_id_created_at_idx" ON "delegation_validations"("task_id", "created_at"); | ||
| CREATE INDEX "delegation_validations_lattice_contract_id_idx" ON "delegation_validations"("lattice_contract_id"); | ||
| CREATE INDEX "delegation_validations_trace_id_idx" ON "delegation_validations"("trace_id"); | ||
| CREATE INDEX "delegation_validations_passed_idx" ON "delegation_validations"("passed"); | ||
|
|
||
| CREATE UNIQUE INDEX "delegation_receipts_user_id_receipt_id_key" ON "delegation_receipts"("user_id", "receipt_id"); | ||
| CREATE INDEX "delegation_receipts_user_id_created_at_idx" ON "delegation_receipts"("user_id", "created_at"); | ||
| CREATE INDEX "delegation_receipts_contract_id_created_at_idx" ON "delegation_receipts"("contract_id", "created_at"); | ||
| CREATE INDEX "delegation_receipts_task_id_created_at_idx" ON "delegation_receipts"("task_id", "created_at"); | ||
| CREATE INDEX "delegation_receipts_status_idx" ON "delegation_receipts"("status"); | ||
|
|
||
| ALTER TABLE "delegation_events" ADD CONSTRAINT "delegation_events_contract_id_fkey" FOREIGN KEY ("contract_id") REFERENCES "delegation_contracts"("id") ON DELETE SET NULL ON UPDATE CASCADE; | ||
| ALTER TABLE "delegation_events" ADD CONSTRAINT "delegation_events_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "delegated_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE; | ||
|
|
||
| ALTER TABLE "delegation_validations" ADD CONSTRAINT "delegation_validations_contract_id_fkey" FOREIGN KEY ("contract_id") REFERENCES "delegation_contracts"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||
| ALTER TABLE "delegation_validations" ADD CONSTRAINT "delegation_validations_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "delegated_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE; | ||
|
|
||
| ALTER TABLE "delegation_receipts" ADD CONSTRAINT "delegation_receipts_contract_id_fkey" FOREIGN KEY ("contract_id") REFERENCES "delegation_contracts"("id") ON DELETE SET NULL ON UPDATE CASCADE; | ||
| ALTER TABLE "delegation_receipts" ADD CONSTRAINT "delegation_receipts_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "delegated_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Migration is not idempotent — missing IF NOT EXISTS guards.
CREATE TYPE, CREATE TABLE, CREATE INDEX, and ALTER TABLE ADD CONSTRAINT statements have no idempotency guards. Re-running this migration (e.g. shadow DB resets, partial reapply) will fail with "already exists" errors. Note CREATE TYPE IF NOT EXISTS is invalid PostgreSQL syntax, so enums need a DO block guard instead.
🛠️ Proposed idempotency fixes
-CREATE TYPE "DelegationEventType" AS ENUM (
- ...
-);
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'DelegationEventType') THEN
+ CREATE TYPE "DelegationEventType" AS ENUM (
+ ...
+ );
+ END IF;
+END
+$$;-CREATE TABLE "delegation_events" (
+CREATE TABLE IF NOT EXISTS "delegation_events" (-CREATE INDEX "delegation_events_user_id_created_at_idx" ON "delegation_events"("user_id", "created_at");
+CREATE INDEX IF NOT EXISTS "delegation_events_user_id_created_at_idx" ON "delegation_events"("user_id", "created_at");-ALTER TABLE "delegation_events" ADD CONSTRAINT "delegation_events_contract_id_fkey" FOREIGN KEY ("contract_id") REFERENCES "delegation_contracts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+ALTER TABLE "delegation_events" DROP CONSTRAINT IF EXISTS "delegation_events_contract_id_fkey";
+ALTER TABLE "delegation_events" ADD CONSTRAINT "delegation_events_contract_id_fkey" FOREIGN KEY ("contract_id") REFERENCES "delegation_contracts"("id") ON DELETE SET NULL ON UPDATE CASCADE;As per coding guidelines, "Migrations must be idempotent. Use IF NOT EXISTS, DROP POLICY IF EXISTS + CREATE POLICY."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| CREATE TYPE "DelegationEventType" AS ENUM ( | |
| 'CONTRACT_CREATED', | |
| 'CONTRACT_ACCEPTED', | |
| 'CONTRACT_STARTED', | |
| 'CONTRACT_COMPLETED', | |
| 'CONTRACT_VERIFIED', | |
| 'CONTRACT_REJECTED', | |
| 'TASK_ASSIGNED', | |
| 'TASK_STARTED', | |
| 'TASK_COMPLETED', | |
| 'TASK_FAILED', | |
| 'HANDOFF_VALIDATED', | |
| 'RECEIPT_ATTACHED', | |
| 'TRUST_SCORED', | |
| 'CHALLENGE_RAISED', | |
| 'AOP_EVENT_RECORDED' | |
| ); | |
| CREATE TYPE "DelegationEventSource" AS ENUM ( | |
| 'ENGRAM', | |
| 'SONDER', | |
| 'LATTICE', | |
| 'RECEIPTS', | |
| 'OPENCLAW' | |
| ); | |
| CREATE TABLE "delegation_events" ( | |
| "id" TEXT NOT NULL, | |
| "user_id" TEXT NOT NULL, | |
| "contract_id" TEXT, | |
| "task_id" TEXT, | |
| "agent_session_key" TEXT, | |
| "event_type" "DelegationEventType" NOT NULL, | |
| "agent_id" TEXT, | |
| "parent_event_id" TEXT, | |
| "trace_id" TEXT, | |
| "source" "DelegationEventSource" NOT NULL DEFAULT 'ENGRAM', | |
| "payload" JSONB NOT NULL DEFAULT '{}'::jsonb, | |
| "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| CONSTRAINT "delegation_events_pkey" PRIMARY KEY ("id") | |
| ); | |
| CREATE TABLE "delegation_validations" ( | |
| "id" TEXT NOT NULL, | |
| "user_id" TEXT NOT NULL, | |
| "contract_id" TEXT NOT NULL, | |
| "task_id" TEXT, | |
| "lattice_contract_id" TEXT, | |
| "trace_id" TEXT, | |
| "passed" BOOLEAN NOT NULL, | |
| "tier" TEXT NOT NULL, | |
| "tiers_run" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], | |
| "duration_ms" INTEGER, | |
| "reason" TEXT, | |
| "confidence" DOUBLE PRECISION, | |
| "provider_failure" BOOLEAN NOT NULL DEFAULT false, | |
| "evidence" JSONB NOT NULL DEFAULT '[]'::jsonb, | |
| "state_contract" JSONB NOT NULL, | |
| "validation_result" JSONB NOT NULL DEFAULT '{}'::jsonb, | |
| "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| CONSTRAINT "delegation_validations_pkey" PRIMARY KEY ("id") | |
| ); | |
| CREATE TABLE "delegation_receipts" ( | |
| "id" TEXT NOT NULL, | |
| "user_id" TEXT NOT NULL, | |
| "contract_id" TEXT, | |
| "task_id" TEXT, | |
| "receipt_id" TEXT NOT NULL, | |
| "status" TEXT NOT NULL, | |
| "claim_summary" TEXT NOT NULL, | |
| "actor_id" TEXT, | |
| "actor_model" TEXT, | |
| "verification_summary" TEXT, | |
| "checks" JSONB NOT NULL DEFAULT '[]'::jsonb, | |
| "risk_level" TEXT, | |
| "artifact_uri" TEXT, | |
| "payload_hash" TEXT, | |
| "artifact_hashes" JSONB NOT NULL DEFAULT '[]'::jsonb, | |
| "receipt" JSONB NOT NULL, | |
| "integrity_status" TEXT, | |
| "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| CONSTRAINT "delegation_receipts_pkey" PRIMARY KEY ("id") | |
| ); | |
| CREATE INDEX "delegation_events_user_id_created_at_idx" ON "delegation_events"("user_id", "created_at"); | |
| CREATE INDEX "delegation_events_contract_id_created_at_idx" ON "delegation_events"("contract_id", "created_at"); | |
| CREATE INDEX "delegation_events_task_id_created_at_idx" ON "delegation_events"("task_id", "created_at"); | |
| CREATE INDEX "delegation_events_agent_id_created_at_idx" ON "delegation_events"("agent_id", "created_at"); | |
| CREATE INDEX "delegation_events_trace_id_idx" ON "delegation_events"("trace_id"); | |
| CREATE INDEX "delegation_events_event_type_idx" ON "delegation_events"("event_type"); | |
| CREATE INDEX "delegation_validations_user_id_created_at_idx" ON "delegation_validations"("user_id", "created_at"); | |
| CREATE INDEX "delegation_validations_contract_id_created_at_idx" ON "delegation_validations"("contract_id", "created_at"); | |
| CREATE INDEX "delegation_validations_task_id_created_at_idx" ON "delegation_validations"("task_id", "created_at"); | |
| CREATE INDEX "delegation_validations_lattice_contract_id_idx" ON "delegation_validations"("lattice_contract_id"); | |
| CREATE INDEX "delegation_validations_trace_id_idx" ON "delegation_validations"("trace_id"); | |
| CREATE INDEX "delegation_validations_passed_idx" ON "delegation_validations"("passed"); | |
| CREATE UNIQUE INDEX "delegation_receipts_user_id_receipt_id_key" ON "delegation_receipts"("user_id", "receipt_id"); | |
| CREATE INDEX "delegation_receipts_user_id_created_at_idx" ON "delegation_receipts"("user_id", "created_at"); | |
| CREATE INDEX "delegation_receipts_contract_id_created_at_idx" ON "delegation_receipts"("contract_id", "created_at"); | |
| CREATE INDEX "delegation_receipts_task_id_created_at_idx" ON "delegation_receipts"("task_id", "created_at"); | |
| CREATE INDEX "delegation_receipts_status_idx" ON "delegation_receipts"("status"); | |
| ALTER TABLE "delegation_events" ADD CONSTRAINT "delegation_events_contract_id_fkey" FOREIGN KEY ("contract_id") REFERENCES "delegation_contracts"("id") ON DELETE SET NULL ON UPDATE CASCADE; | |
| ALTER TABLE "delegation_events" ADD CONSTRAINT "delegation_events_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "delegated_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE; | |
| ALTER TABLE "delegation_validations" ADD CONSTRAINT "delegation_validations_contract_id_fkey" FOREIGN KEY ("contract_id") REFERENCES "delegation_contracts"("id") ON DELETE CASCADE ON UPDATE CASCADE; | |
| ALTER TABLE "delegation_validations" ADD CONSTRAINT "delegation_validations_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "delegated_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE; | |
| ALTER TABLE "delegation_receipts" ADD CONSTRAINT "delegation_receipts_contract_id_fkey" FOREIGN KEY ("contract_id") REFERENCES "delegation_contracts"("id") ON DELETE SET NULL ON UPDATE CASCADE; | |
| ALTER TABLE "delegation_receipts" ADD CONSTRAINT "delegation_receipts_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "delegated_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE; | |
| DO $$ | |
| BEGIN | |
| IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'DelegationEventType') THEN | |
| CREATE TYPE "DelegationEventType" AS ENUM ( | |
| 'CONTRACT_CREATED', | |
| 'CONTRACT_ACCEPTED', | |
| 'CONTRACT_STARTED', | |
| 'CONTRACT_COMPLETED', | |
| 'CONTRACT_VERIFIED', | |
| 'CONTRACT_REJECTED', | |
| 'TASK_ASSIGNED', | |
| 'TASK_STARTED', | |
| 'TASK_COMPLETED', | |
| 'TASK_FAILED', | |
| 'HANDOFF_VALIDATED', | |
| 'RECEIPT_ATTACHED', | |
| 'TRUST_SCORED', | |
| 'CHALLENGE_RAISED', | |
| 'AOP_EVENT_RECORDED' | |
| ); | |
| END IF; | |
| END | |
| $$; | |
| DO $$ | |
| BEGIN | |
| IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'DelegationEventSource') THEN | |
| CREATE TYPE "DelegationEventSource" AS ENUM ( | |
| 'ENGRAM', | |
| 'SONDER', | |
| 'LATTICE', | |
| 'RECEIPTS', | |
| 'OPENCLAW' | |
| ); | |
| END IF; | |
| END | |
| $$; | |
| CREATE TABLE IF NOT EXISTS "delegation_events" ( | |
| "id" TEXT NOT NULL, | |
| "user_id" TEXT NOT NULL, | |
| "contract_id" TEXT, | |
| "task_id" TEXT, | |
| "agent_session_key" TEXT, | |
| "event_type" "DelegationEventType" NOT NULL, | |
| "agent_id" TEXT, | |
| "parent_event_id" TEXT, | |
| "trace_id" TEXT, | |
| "source" "DelegationEventSource" NOT NULL DEFAULT 'ENGRAM', | |
| "payload" JSONB NOT NULL DEFAULT '{}'::jsonb, | |
| "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| CONSTRAINT "delegation_events_pkey" PRIMARY KEY ("id") | |
| ); | |
| CREATE TABLE IF NOT EXISTS "delegation_validations" ( | |
| "id" TEXT NOT NULL, | |
| "user_id" TEXT NOT NULL, | |
| "contract_id" TEXT NOT NULL, | |
| "task_id" TEXT, | |
| "lattice_contract_id" TEXT, | |
| "trace_id" TEXT, | |
| "passed" BOOLEAN NOT NULL, | |
| "tier" TEXT NOT NULL, | |
| "tiers_run" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], | |
| "duration_ms" INTEGER, | |
| "reason" TEXT, | |
| "confidence" DOUBLE PRECISION, | |
| "provider_failure" BOOLEAN NOT NULL DEFAULT false, | |
| "evidence" JSONB NOT NULL DEFAULT '[]'::jsonb, | |
| "state_contract" JSONB NOT NULL, | |
| "validation_result" JSONB NOT NULL DEFAULT '{}'::jsonb, | |
| "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| CONSTRAINT "delegation_validations_pkey" PRIMARY KEY ("id") | |
| ); | |
| CREATE TABLE IF NOT EXISTS "delegation_receipts" ( | |
| "id" TEXT NOT NULL, | |
| "user_id" TEXT NOT NULL, | |
| "contract_id" TEXT, | |
| "task_id" TEXT, | |
| "receipt_id" TEXT NOT NULL, | |
| "status" TEXT NOT NULL, | |
| "claim_summary" TEXT NOT NULL, | |
| "actor_id" TEXT, | |
| "actor_model" TEXT, | |
| "verification_summary" TEXT, | |
| "checks" JSONB NOT NULL DEFAULT '[]'::jsonb, | |
| "risk_level" TEXT, | |
| "artifact_uri" TEXT, | |
| "payload_hash" TEXT, | |
| "artifact_hashes" JSONB NOT NULL DEFAULT '[]'::jsonb, | |
| "receipt" JSONB NOT NULL, | |
| "integrity_status" TEXT, | |
| "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | |
| CONSTRAINT "delegation_receipts_pkey" PRIMARY KEY ("id") | |
| ); | |
| CREATE INDEX IF NOT EXISTS "delegation_events_user_id_created_at_idx" ON "delegation_events"("user_id", "created_at"); | |
| CREATE INDEX IF NOT EXISTS "delegation_events_contract_id_created_at_idx" ON "delegation_events"("contract_id", "created_at"); | |
| CREATE INDEX IF NOT EXISTS "delegation_events_task_id_created_at_idx" ON "delegation_events"("task_id", "created_at"); | |
| CREATE INDEX IF NOT EXISTS "delegation_events_agent_id_created_at_idx" ON "delegation_events"("agent_id", "created_at"); | |
| CREATE INDEX IF NOT EXISTS "delegation_events_trace_id_idx" ON "delegation_events"("trace_id"); | |
| CREATE INDEX IF NOT EXISTS "delegation_events_event_type_idx" ON "delegation_events"("event_type"); | |
| CREATE INDEX IF NOT EXISTS "delegation_validations_user_id_created_at_idx" ON "delegation_validations"("user_id", "created_at"); | |
| CREATE INDEX IF NOT EXISTS "delegation_validations_contract_id_created_at_idx" ON "delegation_validations"("contract_id", "created_at"); | |
| CREATE INDEX IF NOT EXISTS "delegation_validations_task_id_created_at_idx" ON "delegation_validations"("task_id", "created_at"); | |
| CREATE INDEX IF NOT EXISTS "delegation_validations_lattice_contract_id_idx" ON "delegation_validations"("lattice_contract_id"); | |
| CREATE INDEX IF NOT EXISTS "delegation_validations_trace_id_idx" ON "delegation_validations"("trace_id"); | |
| CREATE INDEX IF NOT EXISTS "delegation_validations_passed_idx" ON "delegation_validations"("passed"); | |
| CREATE UNIQUE INDEX IF NOT EXISTS "delegation_receipts_user_id_receipt_id_key" ON "delegation_receipts"("user_id", "receipt_id"); | |
| CREATE INDEX IF NOT EXISTS "delegation_receipts_user_id_created_at_idx" ON "delegation_receipts"("user_id", "created_at"); | |
| CREATE INDEX IF NOT EXISTS "delegation_receipts_contract_id_created_at_idx" ON "delegation_receipts"("contract_id", "created_at"); | |
| CREATE INDEX IF NOT EXISTS "delegation_receipts_task_id_created_at_idx" ON "delegation_receipts"("task_id", "created_at"); | |
| CREATE INDEX IF NOT EXISTS "delegation_receipts_status_idx" ON "delegation_receipts"("status"); | |
| ALTER TABLE "delegation_events" DROP CONSTRAINT IF EXISTS "delegation_events_contract_id_fkey"; | |
| ALTER TABLE "delegation_events" ADD CONSTRAINT "delegation_events_contract_id_fkey" FOREIGN KEY ("contract_id") REFERENCES "delegation_contracts"("id") ON DELETE SET NULL ON UPDATE CASCADE; | |
| ALTER TABLE "delegation_events" DROP CONSTRAINT IF EXISTS "delegation_events_task_id_fkey"; | |
| ALTER TABLE "delegation_events" ADD CONSTRAINT "delegation_events_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "delegated_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE; | |
| ALTER TABLE "delegation_validations" DROP CONSTRAINT IF EXISTS "delegation_validations_contract_id_fkey"; | |
| ALTER TABLE "delegation_validations" ADD CONSTRAINT "delegation_validations_contract_id_fkey" FOREIGN KEY ("contract_id") REFERENCES "delegation_contracts"("id") ON DELETE CASCADE ON UPDATE CASCADE; | |
| ALTER TABLE "delegation_validations" DROP CONSTRAINT IF EXISTS "delegation_validations_task_id_fkey"; | |
| ALTER TABLE "delegation_validations" ADD CONSTRAINT "delegation_validations_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "delegated_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE; | |
| ALTER TABLE "delegation_receipts" DROP CONSTRAINT IF EXISTS "delegation_receipts_contract_id_fkey"; | |
| ALTER TABLE "delegation_receipts" ADD CONSTRAINT "delegation_receipts_contract_id_fkey" FOREIGN KEY ("contract_id") REFERENCES "delegation_contracts"("id") ON DELETE SET NULL ON UPDATE CASCADE; | |
| ALTER TABLE "delegation_receipts" DROP CONSTRAINT IF EXISTS "delegation_receipts_task_id_fkey"; | |
| ALTER TABLE "delegation_receipts" ADD CONSTRAINT "delegation_receipts_task_id_fkey" FOREIGN KEY ("task_id") REFERENCES "delegated_tasks"("id") ON DELETE SET NULL ON UPDATE CASCADE; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@prisma/migrations/20260702143000_delegation_ledger/migration.sql` around
lines 4 - 116, The migration in migration.sql is not idempotent, so rerunning it
can fail on existing enums, tables, indexes, or constraints. Update the
DelegationEventType and DelegationEventSource creation to use a DO block guard,
and add existence checks around the delegation_events, delegation_validations,
and delegation_receipts table creation. Also guard every CREATE INDEX and ALTER
TABLE ADD CONSTRAINT in this migration so repeated applies do not raise
already-exists errors.
Source: Coding guidelines
| ALTER TABLE "delegation_events" ENABLE ROW LEVEL SECURITY; | ||
| CREATE POLICY account_isolation ON "delegation_events" FOR ALL USING ( | ||
| rls_account_id() IS NULL OR "user_id" IN (SELECT rls_user_ids()) | ||
| ); | ||
|
|
||
| ALTER TABLE "delegation_validations" ENABLE ROW LEVEL SECURITY; | ||
| CREATE POLICY account_isolation ON "delegation_validations" FOR ALL USING ( | ||
| rls_account_id() IS NULL OR "user_id" IN (SELECT rls_user_ids()) | ||
| ); | ||
|
|
||
| ALTER TABLE "delegation_receipts" ENABLE ROW LEVEL SECURITY; | ||
| CREATE POLICY account_isolation ON "delegation_receipts" FOR ALL USING ( | ||
| rls_account_id() IS NULL OR "user_id" IN (SELECT rls_user_ids()) | ||
| ); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Policy creation missing DROP POLICY IF EXISTS guard.
CREATE POLICY account_isolation will fail on re-run since the policy already exists. Guideline requires DROP POLICY IF EXISTS before CREATE POLICY.
🛠️ Proposed fix
ALTER TABLE "delegation_events" ENABLE ROW LEVEL SECURITY;
+DROP POLICY IF EXISTS account_isolation ON "delegation_events";
CREATE POLICY account_isolation ON "delegation_events" FOR ALL USING (
rls_account_id() IS NULL OR "user_id" IN (SELECT rls_user_ids())
);(repeat for delegation_validations and delegation_receipts)
As per coding guidelines, "DROP POLICY IF EXISTS + CREATE POLICY. Never use CREATE POLICY IF NOT EXISTS — that's invalid PostgreSQL syntax."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@prisma/migrations/20260702143000_delegation_ledger/migration.sql` around
lines 118 - 131, The RLS policy setup for delegation_events,
delegation_validations, and delegation_receipts should be made idempotent by
dropping any existing account_isolation policy before recreating it. Update the
migration statements around the CREATE POLICY calls to use DROP POLICY IF EXISTS
for each table first, then CREATE POLICY account_isolation, so re-running the
migration won’t fail; apply this pattern consistently across all three tables in
the migration.
Source: Coding guidelines
| validationResult Json @default("{}") @map("validation_result") | ||
| createdAt DateTime @default(now()) @map("created_at") | ||
|
|
||
| contract DelegationContract @relation(fields: [contractId], references: [id], onDelete: Cascade) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== prisma/schema.prisma around delegation models ==\n'
rg -n -C 4 'DelegationValidation|DelegationReceipt|DelegationEvent|contractId|onDelete' prisma/schema.prisma
printf '\n== migration.sql around line 112 and related delegation FKs ==\n'
rg -n -C 4 'DelegationValidation|DelegationReceipt|DelegationEvent|FOREIGN KEY|ON DELETE|contractId' migration.sqlRepository: heybeaux/engram
Length of output: 20858
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== repository files matching migration.sql ==\n'
fd -HI '^migration\.sql$' .
printf '\n== files mentioning delegation_validations FK or ON DELETE ==\n'
rg -n -C 3 'delegation_validations|delegation_receipts|delegation_events|ON DELETE|FOREIGN KEY|contract_id' . -g '!node_modules' -g '!dist' -g '!build'
printf '\n== prisma directory tree ==\n'
find prisma -maxdepth 3 -type f | sortRepository: heybeaux/engram
Length of output: 50373
Use SetNull for DelegationValidation.contract
DelegationValidation rows are deleted when a contract is removed. If these records are evidence, make contractId nullable and switch this relation to SetNull like DelegationEvent and DelegationReceipt.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@prisma/schema.prisma` at line 2227, The DelegationValidation.contract
relation is currently using Cascade deletion, so removing a contract deletes
validation evidence rows too. Update the DelegationValidation model to make
contractId nullable and change the contract relation to SetNull, following the
same pattern used by DelegationEvent and DelegationReceipt, and make sure any
related schema references and generated migration reflect the nullable foreign
key.
| await this.ledger?.recordEvent(userId, { | ||
| eventType: 'CONTRACT_CREATED', | ||
| source: 'ENGRAM', | ||
| contractId: contract.id, | ||
| agentId: contract.delegator, | ||
| payload: { | ||
| delegator: contract.delegator, | ||
| delegate: contract.delegate, | ||
| taskDescription: contract.taskDescription, | ||
| terms: contract.terms, | ||
| }, | ||
| }); | ||
| return contract; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Unhandled ledger errors will fail contract create/update even though the primary write already succeeded.
await this.ledger?.recordEvent(...) is called after the contract has already been created (Line 29-38) / updated (Line 101-104) in the database, but no try/catch guards it. If the ledger call throws (DB hiccup, bad enum mapping, etc.), the whole create/update method rejects and the API returns an error to the caller — despite the contract row already being persisted with the new state. This is a best-effort audit side-effect and shouldn't be able to fail the primary operation.
🔧 Proposed fix (apply to both call sites)
- await this.ledger?.recordEvent(userId, {
- eventType: 'CONTRACT_CREATED',
- source: 'ENGRAM',
- contractId: contract.id,
- agentId: contract.delegator,
- payload: {
- delegator: contract.delegator,
- delegate: contract.delegate,
- taskDescription: contract.taskDescription,
- terms: contract.terms,
- },
- });
+ try {
+ await this.ledger?.recordEvent(userId, {
+ eventType: 'CONTRACT_CREATED',
+ source: 'ENGRAM',
+ contractId: contract.id,
+ agentId: contract.delegator,
+ payload: {
+ delegator: contract.delegator,
+ delegate: contract.delegate,
+ taskDescription: contract.taskDescription,
+ terms: contract.terms,
+ },
+ });
+ } catch (err) {
+ // Best-effort audit trail: don't fail contract creation on ledger errors.
+ }Also applies to: 105-119
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/delegation/contract.service.ts` around lines 39 - 51, Wrap the
best-effort ledger audit call in ContractService so it cannot fail the primary
database write: in both the create and update paths around
this.recordEvent/ledger?.recordEvent call, catch and swallow/log any thrown
error after the contract has already been persisted. Use the ContractService
create/update methods and the ledger.recordEvent invocation as the key
locations, and keep the contract return path unchanged even if the ledger write
fails.
| const validation = await this.prisma.delegationValidation.create({ | ||
| data: { | ||
| userId, | ||
| contractId, | ||
| taskId: dto.taskId ?? null, | ||
| latticeContractId: latticeContractId ?? null, | ||
| traceId: traceId ?? null, | ||
| passed, | ||
| tier, | ||
| tiersRun, | ||
| durationMs: durationMs ?? null, | ||
| reason: reason ?? null, | ||
| confidence: confidence ?? null, | ||
| providerFailure, | ||
| evidence, | ||
| stateContract, | ||
| validationResult, | ||
| }, | ||
| }); | ||
|
|
||
| await this.recordEvent(userId, { | ||
| eventType: 'HANDOFF_VALIDATED', | ||
| source: 'LATTICE', | ||
| contractId, | ||
| taskId: dto.taskId, | ||
| agentId: contract.delegate, | ||
| traceId: validation.traceId ?? undefined, | ||
| payload: { | ||
| validationId: validation.id, | ||
| latticeContractId: validation.latticeContractId, | ||
| passed: validation.passed, | ||
| tier: validation.tier, | ||
| tiersRun: validation.tiersRun, | ||
| reason: validation.reason, | ||
| confidence: validation.confidence, | ||
| providerFailure: validation.providerFailure, | ||
| }, | ||
| }); | ||
|
|
||
| await this.recordTrustFromValidation(userId, contract, validation); | ||
| return validation; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Multi-step ledger writes aren't atomic.
recordValidation performs delegationValidation.create → recordEvent → recordTrustFromValidation (itself a trustSignal.create + another recordEvent) as separate, unwrapped calls. Same pattern in attachReceipt (upsert → event → trust). If any later step throws (e.g., a transient DB error on the trust-signal insert), the validation/receipt row is already committed but the corresponding lifecycle event and/or trust signal is silently missing — corrupting the append-only evidence trail this ledger is meant to guarantee.
Wrap each flow in this.prisma.$transaction(async (tx) => {...}) and thread tx through recordEvent/recordTrustFromValidation/recordTrustFromReceipt instead of always using this.prisma.
Also applies to: 129-210
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/delegation/delegation-ledger.service.ts` around lines 86 - 127, The
ledger write flow is not atomic because `recordValidation` (and the same pattern
in `attachReceipt`) commits multiple Prisma operations separately. Wrap the full
sequence in `this.prisma.$transaction(async (tx) => {...})` and make
`recordValidation`, `recordEvent`, `recordTrustFromValidation`, and
`recordTrustFromReceipt` use the transaction client instead of always calling
`this.prisma`. Ensure the validation/receipt row, lifecycle event, and
trust-signal writes all succeed or fail together in `DelegationLedgerService`.
| async attachReceipt(userId: string, taskId: string, dto: AttachReceiptDto) { | ||
| const task = await this.assertTask(userId, taskId); | ||
| const contractId = dto.contractId ?? task.contractId ?? undefined; | ||
| if (contractId) await this.assertContract(userId, contractId); | ||
|
|
||
| const receipt = dto.receipt ?? {}; | ||
| const receiptId = this.stringField(receipt.id); | ||
| if (!receiptId) throw new BadRequestException('receipt.id is required'); | ||
|
|
||
| const claim = this.asRecord(receipt.claim); | ||
| const actor = this.asRecord(receipt.actor); | ||
| const verification = this.asRecord(receipt.verification); | ||
| const risk = this.asRecord(receipt.risk); | ||
| const integrity = this.asRecord(receipt.integrity); | ||
| const checks = this.arrayField(verification.checks) ?? []; | ||
| const artifactHashes = this.arrayField(integrity.artifacts) ?? []; | ||
| const payloadHash = this.stringField(integrity.receipt_payload_sha256); | ||
| const integrityStatus = payloadHash | ||
| ? this.computeReceiptPayloadHash(receipt) === payloadHash | ||
| ? 'clean' | ||
| : 'payload_mismatch' | ||
| : 'missing_integrity'; | ||
|
|
||
| const row = await this.prisma.delegationReceipt.upsert({ | ||
| where: { userId_receiptId: { userId, receiptId } }, | ||
| create: { | ||
| userId, | ||
| contractId: contractId ?? null, | ||
| taskId, | ||
| receiptId, | ||
| status: this.stringField(receipt.status) ?? 'needs-review', | ||
| claimSummary: this.stringField(claim.summary) ?? 'Delegation receipt', | ||
| actorId: this.stringField(actor.id), | ||
| actorModel: this.stringField(actor.model), | ||
| verificationSummary: this.stringField(verification.summary), | ||
| checks, | ||
| riskLevel: this.stringField(risk.level), | ||
| artifactUri: dto.artifactUri ?? null, | ||
| payloadHash, | ||
| artifactHashes, | ||
| receipt, | ||
| integrityStatus, | ||
| }, | ||
| update: { | ||
| contractId: contractId ?? null, | ||
| taskId, | ||
| status: this.stringField(receipt.status) ?? 'needs-review', | ||
| claimSummary: this.stringField(claim.summary) ?? 'Delegation receipt', | ||
| actorId: this.stringField(actor.id), | ||
| actorModel: this.stringField(actor.model), | ||
| verificationSummary: this.stringField(verification.summary), | ||
| checks, | ||
| riskLevel: this.stringField(risk.level), | ||
| artifactUri: dto.artifactUri ?? null, | ||
| payloadHash, | ||
| artifactHashes, | ||
| receipt, | ||
| integrityStatus, | ||
| }, | ||
| }); | ||
|
|
||
| await this.recordEvent(userId, { | ||
| eventType: 'RECEIPT_ATTACHED', | ||
| source: 'RECEIPTS', | ||
| contractId, | ||
| taskId, | ||
| agentId: task.assignedTo, | ||
| payload: { | ||
| delegationReceiptId: row.id, | ||
| receiptId: row.receiptId, | ||
| status: row.status, | ||
| claimSummary: row.claimSummary, | ||
| integrityStatus: row.integrityStatus, | ||
| verificationSummary: row.verificationSummary, | ||
| checks, | ||
| riskLevel: row.riskLevel, | ||
| }, | ||
| }); | ||
|
|
||
| await this.recordTrustFromReceipt(userId, task, row); | ||
| return row; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Receipt re-attachment isn't idempotent at the event/trust layer.
The delegationReceipt.upsert (Lines 152-188) is safe to retry (unique on userId_receiptId), but the subsequent recordEvent('RECEIPT_ATTACHED', ...) (Lines 190-206) and recordTrustFromReceipt (Line 208) run unconditionally on every call — including a retried/duplicate submission of the exact same receipt. A network retry or duplicate client submission of an already-clean self-verified receipt would emit a second RECEIPT_ATTACHED event and award a second SUCCESS trust signal (weight 1.3), inflating the agent's trust score for the same underlying work.
Consider checking whether the row previously existed (e.g. findUnique before the upsert) and skip the event/trust emission when the receipt content is unchanged.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/delegation/delegation-ledger.service.ts` around lines 129 - 210, The
attachReceipt flow in DelegationLedgerService is not idempotent beyond the
delegationsReceipt upsert, because recordEvent and recordTrustFromReceipt always
run even when the same receipt is resent. Update attachReceipt to detect whether
the existing receipt row already matches the incoming receipt content (for
example by checking before delegationsReceipt.upsert or comparing the returned
row fields), and only emit the RECEIPT_ATTACHED event and trust update when the
receipt is new or changed. Use the attachReceipt, recordEvent, and
recordTrustFromReceipt paths to keep duplicate submissions from creating
duplicate events or trust signals.
| await this.ledger?.recordEvent(userId, { | ||
| eventType: 'TASK_ASSIGNED', | ||
| source: 'ENGRAM', | ||
| contractId: task.contractId ?? undefined, | ||
| taskId: task.id, | ||
| agentId: task.assignedTo, | ||
| payload: { | ||
| assignedBy: task.assignedBy, | ||
| assignedTo: task.assignedTo, | ||
| taskDescription: task.taskDescription, | ||
| deadline: task.deadline?.toISOString?.() ?? task.deadline, | ||
| }, | ||
| }); | ||
| return task; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Unguarded ledger write can fail an already-successful task mutation.
this.ledger?.recordEvent(...) is awaited directly after prisma.delegatedTask.create/update already committed. If the ledger write throws (e.g., assertContract/assertTask not-found, transient DB error), the whole create/update call rejects even though the task row is already persisted — the caller sees an error for an operation that actually succeeded, which risks duplicate task creation on retry.
🛠️ Proposed fix
- await this.ledger?.recordEvent(userId, {
- eventType: 'TASK_ASSIGNED',
- source: 'ENGRAM',
- contractId: task.contractId ?? undefined,
- taskId: task.id,
- agentId: task.assignedTo,
- payload: {
- assignedBy: task.assignedBy,
- assignedTo: task.assignedTo,
- taskDescription: task.taskDescription,
- deadline: task.deadline?.toISOString?.() ?? task.deadline,
- },
- });
+ try {
+ await this.ledger?.recordEvent(userId, {
+ eventType: 'TASK_ASSIGNED',
+ source: 'ENGRAM',
+ contractId: task.contractId ?? undefined,
+ taskId: task.id,
+ agentId: task.assignedTo,
+ payload: {
+ assignedBy: task.assignedBy,
+ assignedTo: task.assignedTo,
+ taskDescription: task.taskDescription,
+ deadline: task.deadline?.toISOString?.() ?? task.deadline,
+ },
+ });
+ } catch {
+ // Best-effort audit write; don't fail task creation on ledger errors.
+ }Apply the same try/catch wrap around the update() ledger call.
Also applies to: 63-76
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/delegation/task.service.ts` around lines 28 - 41, The awaited ledger
write in TaskService after the task create/update commit can throw and
incorrectly fail an already-persisted mutation. Wrap the
`this.ledger?.recordEvent(...)` call in `TaskService.create` and
`TaskService.update` with the same `try/catch` handling, so ledger failures are
logged/ignored or otherwise isolated from the Prisma transaction result. Use the
`TaskService`, `create`, `update`, and `recordEvent` symbols to find both call
sites and keep the task mutation success path from being rejected by ledger
errors.
Summary
Adds first-class delegation ledger infrastructure so Engram can track delegated work across agents with evidence rather than prose-only memories.
This bridges the existing formal delegation system with Sonder/AOP-style events, Lattice StateContract validation evidence, Receipts proof-of-work, and Engram trust signals/reports.
What changed
DelegationEvent— append-only lifecycle/AOP-Sonder event logDelegationValidation— LatticeStateContract+ validation result/evidenceDelegationReceipt— Receipts proof-of-work, checks, risk, integrity statusengram_appgrants for the new ledger tables.DelegationLedgerServiceand controller endpoints:POST /v1/delegation/eventsPOST /v1/delegation/contracts/:id/validationsPOST /v1/delegation/tasks/:id/receiptsGET /v1/delegation/tasks/:id/trust-reportGET /v1/delegation/trust-reports/:agentIdValidation
pnpm prisma validatepnpm exec jest src/delegation --runInBand --forceExitpnpm buildpnpm exec eslint ...touched delegation filesno-unsafe-*warnings onlyNotes
This intentionally does not create a third unrelated delegation stack. It adds ledger tables and services around the existing formal delegation primitives so identity/trust/dashboard work can converge on one evidence trail.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation