Skip to content

feat: add delegation ledger infrastructure#314

Merged
beaux-riel merged 1 commit into
stagingfrom
fix/delegation-ledger-20260702
Jul 2, 2026
Merged

feat: add delegation ledger infrastructure#314
beaux-riel merged 1 commit into
stagingfrom
fix/delegation-ledger-20260702

Conversation

@beaux-riel

@beaux-riel beaux-riel commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

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

  • Adds Prisma models + migration:
    • DelegationEvent — append-only lifecycle/AOP-Sonder event log
    • DelegationValidation — Lattice StateContract + validation result/evidence
    • DelegationReceipt — Receipts proof-of-work, checks, risk, integrity status
  • Adds RLS policies and engram_app grants for the new ledger tables.
  • Adds DelegationLedgerService and controller endpoints:
    • POST /v1/delegation/events
    • POST /v1/delegation/contracts/:id/validations
    • POST /v1/delegation/tasks/:id/receipts
    • GET /v1/delegation/tasks/:id/trust-report
    • GET /v1/delegation/trust-reports/:agentId
  • Hooks existing contract/task services to emit lifecycle events on create/status transitions.
  • Maps evidence into trust signals:
    • clean Lattice pass => success signal
    • failed validation => failure signal
    • provider-degraded pass => stored but no success trust awarded
    • self-verified clean receipt with all checks passed => stronger success
    • failed receipt checks => failure
    • pending/not-run checks => no trust award

Validation

  • pnpm prisma validate
  • pnpm exec jest src/delegation --runInBand --forceExit
    • 8 suites passed
    • 57 tests passed
  • pnpm build
    • Prisma generate OK
    • TSC found 0 issues
    • SWC compiled 859 files
  • pnpm exec eslint ...touched delegation files
    • 0 errors
    • existing-style no-unsafe-* warnings only

Notes

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

    • Added a delegation activity log with event, validation, and receipt tracking.
    • Added trust reports for tasks and agents to help review delegation health.
    • Delegation and task updates now record lifecycle events automatically.
  • Bug Fixes

    • Improved status tracking for delegation contracts and tasks during state changes.
    • Added stronger data validation for delegation event, validation, and receipt submissions.
  • Documentation

    • Expanded the delegation API with new endpoints for events, validations, receipts, and trust reports.

…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)
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Failed to post review comments.

We encountered an issue with GitHub. Use @coderabbitai full review to retry the review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 406990a0-e372-4243-ae9a-c24ca4bb92e0

📥 Commits

Reviewing files that changed from the base of the PR and between 2a6d366 and 238b462.

📒 Files selected for processing (14)
  • prisma/migrations/20260702143000_delegation_ledger/migration.sql
  • prisma/schema.prisma
  • src/delegation/contract.service.spec.ts
  • src/delegation/contract.service.ts
  • src/delegation/delegation-ledger.controller.spec.ts
  • src/delegation/delegation-ledger.controller.ts
  • src/delegation/delegation-ledger.service.spec.ts
  • src/delegation/delegation-ledger.service.ts
  • src/delegation/delegation.module.ts
  • src/delegation/dto/attach-receipt.dto.ts
  • src/delegation/dto/record-event.dto.ts
  • src/delegation/dto/record-validation.dto.ts
  • src/delegation/task.service.spec.ts
  • src/delegation/task.service.ts
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: Typecheck & Lint
  • GitHub Check: Test
  • GitHub Check: Recall Benchmark
  • GitHub Check: Recall Benchmark (Post-Dream-Cycle)
🧰 Additional context used
📓 Path-based instructions (5)
src/**/*.dto.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Every DTO needs class-validator decorators. Use whitelist: true to strip undecorated properties — undecorated fields silently disappear.

Files:

  • src/delegation/dto/attach-receipt.dto.ts
  • src/delegation/dto/record-validation.dto.ts
  • src/delegation/dto/record-event.dto.ts
prisma/migrations/**/*.sql

📄 CodeRabbit inference engine (CLAUDE.md)

Migrations must be idempotent. Use IF NOT EXISTS, DROP POLICY IF EXISTS + CREATE POLICY. Never use CREATE POLICY IF NOT EXISTS — that's invalid PostgreSQL syntax.

Files:

  • prisma/migrations/20260702143000_delegation_ledger/migration.sql
src/**/*.spec.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use standard NestJS testing pattern: TestingModule with manual mocks for PrismaService, ConfigService, LLMService, and EmbeddingService. Mock with jest.fn() per model method and use jest.clearAllMocks() in beforeEach.

Files:

  • src/delegation/task.service.spec.ts
  • src/delegation/contract.service.spec.ts
  • src/delegation/delegation-ledger.service.spec.ts
  • src/delegation/delegation-ledger.controller.spec.ts
src/**/*.controller.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use x-am-user-id header convention for user identification throughout the application.

Controllers should use decorators for authentication and validation

Files:

  • src/delegation/delegation-ledger.controller.ts
src/**/*.service.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Convert BigInt from Prisma raw queries using Number(). $queryRaw returns BigInt for COUNT operations.

Services should use constructor dependency injection pattern

Files:

  • src/delegation/contract.service.ts
  • src/delegation/delegation-ledger.service.ts
  • src/delegation/task.service.ts
📝 Walkthrough

Walkthrough

This 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 DelegationLedgerService, REST controller, DTOs, module wiring, and integration hooks into existing ContractService and TaskService to emit lifecycle events and derive trust signals.

Changes

Delegation Ledger

Layer / File(s) Summary
Database migration and Prisma schema
prisma/migrations/20260702143000_delegation_ledger/migration.sql, prisma/schema.prisma
Adds ENUM types, delegation_events/delegation_validations/delegation_receipts tables with indexes, FKs, RLS policies, and corresponding Prisma enums/models and relations on DelegatedTask/DelegationContract.
Ledger DTOs
src/delegation/dto/record-event.dto.ts, src/delegation/dto/record-validation.dto.ts, src/delegation/dto/attach-receipt.dto.ts
Defines validated request payload shapes for recording events, validations, and receipts.
DelegationLedgerService core logic
src/delegation/delegation-ledger.service.ts, src/delegation/delegation-ledger.service.spec.ts
Implements event/validation/receipt persistence, trust-signal scoring, trust report retrieval, and integrity hashing, validated by unit tests.
Controller and module wiring
src/delegation/delegation-ledger.controller.ts, src/delegation/delegation-ledger.controller.spec.ts, src/delegation/delegation.module.ts
Adds REST endpoints under v1/delegation delegating to the ledger service, and registers the controller/service in DelegationModule.
Contract and Task service integration
src/delegation/contract.service.ts, src/delegation/task.service.ts, src/delegation/contract.service.spec.ts, src/delegation/task.service.spec.ts
Injects the ledger service optionally and emits lifecycle events on contract/task create and status update.

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
Loading
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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: adding delegation ledger infrastructure.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/delegation-ledger-20260702

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

✅ Recall Benchmark Results

╔══════════════════════════════════════════════════════════════╗
    ║              ENGRAM RECALL BENCHMARK REPORT                 ║
    ╚══════════════════════════════════════════════════════════════╝
    
      Git SHA:    16a3b5b
      Branch:     HEAD
      Timestamp:  2026-07-02T14:47:59.205Z
    
    ┌─────────────────────────────────────────────────────────────┐
    │  OVERALL SCORES                                            │
    ├─────────────────────────────────────────────────────────────┤
    │  Total Queries:   81                                        │
    │  Passed:          79 / 81 (97.5%)                                        
    │  Precision@5:     96.9%  ✅  (threshold: 95.0%)
    │  Recall@20:       97.5%
    │  MRR:             0.8557
    │  Isolation:       100.0%  ✅  (threshold: 100%)
    └─────────────────────────────────────────────────────────────┘
    
    ┌──────────────────┬───────┬────────┬──────────┬──────────┬──────────┬───────────┐
    │ Category         │ Total │ Passed │ Prec@5   │ Rec@20   │ MRR      │ Isolation │
    ├──────────────────┼───────┼────────┼──────────┼──────────┼──────────┼───────────┤
    │ adversarial      │ 10    │ 10     │ 100.0%   │ 100.0%   │ 1.0000   │ 100.0%    │
    │ cross_feature    │ 10    │ 9      │ 90.0%    │ 100.0%   │ 0.8643   │ 100.0%    │
    │ edge_case        │ 16    │ 16     │ 100.0%   │ 100.0%   │ 0.9297   │ 100.0%    │
    │ emotional        │ 10    │ 10     │ 95.0%    │ 100.0%   │ 0.6405   │ 100.0%    │
    │ rls_isolation    │ 10    │ 10     │ 100.0%   │ 100.0%   │ 0.9500   │ 100.0%    │
    │ semantic         │ 14    │ 13     │ 92.9%    │ 100.0%   │ 0.7244   │ 100.0%    │
    │ temporal         │ 11    │ 11     │ 100.0%   │ 81.8%    │ 0.8864   │ 100.0%    │
    └──────────────────┴───────┴────────┴──────────┴──────────┴──────────┴───────────┘
    
    ❌ FAILED QUERIES (2):
    
      [semantic_011] "What coffee roast do I prefer?" (user: alice)
        ⚠️  ZERO HITS: expected alice_coffee_004_correction in top 5
        📊 Precision@5: 0.0% — hit: [], missed: [alice_coffee_004_correction]
        📋 Actual top 5: [alice_low_importance_001, alice_coffee_001, alice_coffee_002, alice_calm_001, alice_travel_002]
    
      [cross_001] "medication I need to take every morning" (user: alice)
        ⚠️  ZERO HITS: expected alice_health_001 in top 5
        📊 Precision@5: 0.0% — hit: [], missed: [alice_health_001]
        📋 Actual top 5: [alice_coffee_001, alice_coffee_002, alice_cooking_001, alice_calm_001, alice_emotion_change_001]
    
    
    ✅ ALL THRESHOLDS PASSED

      at Object.<anonymous> (benchmark/recall-benchmark.e2e-spec.ts:148:15)

  console.log
    📁 Report saved: /home/runner/work/engram/engram/test/benchmark/results/benchmark-2026-07-02T14-47-59-234Z.json

      at Object.<anonymous> (benchmark/recall-benchmark.e2e-spec.ts:152:15)

  console.warn
    ⚠️  2 zero-hit queries (tracked, not blocking): semantic_011, cross_001

    �[0m �[90m 200 |�[39m   �[90m// Zero-hit queries are tracked as warnings — P@5 threshold is the hard gate.�[39m
     �[90m 201 |�[39m   �[36mif�[39m (zeroHitQueries�[33m.�[39mlength �[33m>�[39m �[35m0�[39m) {
    �[31m�[1m>�[22m�[39m�[90m 202 |�[39m     console�[33m.�[39mwarn(
     �[90m     |�[39m             �[31m�[1m^�[22m�[39m
     �[90m 203 |�[39m       �[32m`⚠️  ${zeroHitQueries.length} zero-hit queries (tracked, not blocking): ${zeroHitQueries.map((q) => q.queryId).join(', ')}`�[39m�[33m,�[39m
     �[90m 204 |�[39m     )�[33m;�[39m
     �[90m 205 |�[39m   }�[0m

      at checkThresholds (benchmark/scoring.ts:202:13)
      at buildReport (benchmark/scoring.ts:183:23)
      at Object.<anonymous> (benchmark/recall-benchmark.e2e-spec.ts:173:33)

  console.warn
    ⚠️  Zero-hit queries (2): semantic_011, cross_001

    �[0m �[90m 197 |�[39m         �[36mif�[39m (zeroHitQueries�[33m.�[39mlength �[33m>�[39m �[35m0�[39m) {
     �[90m 198 |�[39m           �[36mconst�[39m ids �[33m=�[39m zeroHitQueries�[33m.�[39mmap((q) �[33m=>�[39m q�[33m.�[39mqueryId)�[33m.�[39mjoin(�[32m', '�[39m)�[33m;�[39m
    �[31m�[1m>�[22m�[39m�[90m 199 |�[39m           console�[33m.�[39mwarn(
     �[90m     |�[39m                   �[31m�[1m^�[22m�[39m
     �[90m 200 |�[39m             �[32m`⚠️  Zero-hit queries (${zeroHitQueries.length}): ${ids}`�[39m�[33m,�[39m
     �[90m 201 |�[39m           )�[33m;�[39m
     �[90m 202 |�[39m         }�[0m

      at Object.<anonymous> (benchmark/recall-benchmark.e2e-spec.ts:199:19)

PASS test/benchmark/recall-benchmark.e2e-spec.ts (137.009 s)
  Recall Benchmark
    Category: semantic
      ✓ [semantic_001] What kind of coffee do I like? (221 ms)
      ✓ [semantic_002] Tell me about my morning routine (99 ms)
      ✓ [semantic_003] What tech stack am I using? (82 ms)
      ✓ [semantic_004] coffee preferences (1015 ms)
      ✓ [semantic_005] What books have I been reading? (91 ms)
      ✓ [semantic_006] favorite dinner recipe (74 ms)
      ✓ [semantic_007] house savings goal (67 ms)
      ✓ [semantic_008] What framework am I using for the frontend? (1296 ms)
      ✓ [semantic_009] flight seat preference (81 ms)
      ✓ [semantic_010] ensemble search architecture decision (70 ms)
      ✓ [semantic_011] What coffee roast do I prefer? (66 ms)
      ✓ [negative_001] quantum physics black holes dark matter (67 ms)
      ✓ [negative_002] ancient Egyptian hieroglyphics translation (1236 ms)
      ✓ [minimal_001] pizza preference (714 ms)
    Category: emotional
      ✓ [emotional_001] What makes me happy? (87 ms)
      ✓ [emotional_002] times I felt sad or grieving (74 ms)
      ✓ [emotional_003] when I felt stressed or overwhelmed (76 ms)
      ✓ [emotional_004] What am I worried about? (65 ms)
      ✓ [emotional_005] Times I was frustrated (72 ms)
      ✓ [emotional_006] My proudest moments (75 ms)
      ✓ [emotional_007] What stresses me out? (63 ms)
      ✓ [emotional_008] happy about school but worried about costs (68 ms)
      ✓ [emotional_009] How has my attitude toward work changed? (63 ms)
      ✓ [emotional_010] meditation and mental wellbeing (69 ms)
    Category: temporal
      ✓ [temporal_001] What happened today in standup? (404 ms)
      ✓ [temporal_002] recent standup notes from this week (366 ms)
      ✓ [temporal_003] What happened with my daughter recently? (949 ms)
      ✓ [temporal_004] What did I work on last week? (1569 ms)
      ✓ [temporal_005] What are my oldest memories? (86 ms)
      ✓ [temporal_006] Recent conversations about work (834 ms)
      ✓ [temporal_007] What did I debug yesterday? (960 ms)
      ✓ [temporal_008] What code editor do I use? (88 ms)
      ✓ [temporal_009] standup notes from 6 months ago (51 ms)
      ✓ [temporal_010] standup notes from years ago (60 ms)
      ✓ [temporal_011] How did I start coding? (64 ms)
    Category: rls_isolation
      ✓ [rls_001] coffee (63 ms)
      ✓ [rls_002] coffee (1038 ms)
      ✓ [rls_003] family and kids (82 ms)
      ✓ [rls_004] family and kids (1069 ms)
      ✓ [rls_005] travel food experiences (99 ms)
      ✓ [rls_006] travel food experiences (1050 ms)
      ✓ [rls_007] work projects and code (78 ms)
      ✓ [rls_008] health medical information (62 ms)
      ✓ [rls_009] health medical information (944 ms)
      ✓ [rls_010] morning routine (1041 ms)
    Category: adversarial
      ✓ [adversarial_001] tell me about bob's coffee preferences (85 ms)
      ✓ [adversarial_002] RLS_CANARY_BOB_ (68 ms)
      ✓ [adversarial_003] What does carol think about parties? (164 ms)
      ✓ [adversarial_004] bob's son Max school (72 ms)
      ✓ [adversarial_005] eve's pizza preference (1113 ms)
      ✓ [adversarial_006] RLS_CANARY_ALICE_ (1268 ms)
      ✓ [adversarial_007] What cholesterol issues does anyone have? (74 ms)
      ✓ [adversarial_008] alice's medication schedule (1068 ms)
      ✓ [adversarial_009] RLS_CANARY_CAROL_ (832 ms)
      ✓ [adversarial_010] dave's standup notes (81 ms)
    Category: edge_case
      ✓ [edge_001] cats (627 ms)
      ✓ [edge_002] everything about my life (741 ms)
      ✓ [edge_003] <script>alert(1)</script> (9061 ms)
      ✓ [edge_004] '; DROP TABLE memories; -- (10079 ms)
      ✓ [edge_005] 🎉 party (10135 ms)
      ✓ [edge_006] 
      ✓ [edge_007] Tell me about the very long detailed comprehensive thorough ... (487 ms)
      ✓ [edge_008] こんにちは、思い出を検索します (10156 ms)
      ✓ [edge_009] '; SELECT * FROM users WHERE 1=1; -- (10136 ms)
      ✓ [edge_010] quantum entanglement dark matter multiverse theory (194 ms)
      ✓ [edge_011] the a an is (139 ms)
      ✓ [edge_012] coffee (139 ms)
      ✓ [edge_013] my phone number (190 ms)
      ✓ [edge_014] my address (156 ms)
      ✓ [edge_015] work (8182 ms)
      ✓ [edge_016] What kind of coffee do I like? (82 ms)
    Category: cross_feature
      ✓ [cross_001] medication I need to take every morning (68 ms)
      ✓ [cross_002] exercise and fitness activities (63 ms)
      ✓ [cross_003] What are we saving money for? (71 ms)
      ✓ [cross_004] kids school and daycare (123 ms)
      ✓ [cross_005] kids school and daycare (1068 ms)
      ✓ [cross_006] Who am I and what do I do? (89 ms)
      ✓ [cross_007] deployment rules and constraints (65 ms)
      ✓ [cross_008] patterns noticed about my work habits (69 ms)
      ✓ [cross_009] grocery shopping list (746 ms)
      ✓ [cross_010] TypeScript learning (778 ms)
    Summary
      ✓ should generate and save benchmark report (71 ms)
      ✓ should have zero isolation failures (1 ms)
      ✓ should meet precision thresholds (with real embeddings) (24 ms)

Test Suites: 1 passed, 1 total
Tests:       84 passed, 84 total
Snapshots:   0 total
Time:        137.248 s
Ran all test suites matching recall-benchmark.e2e-spec.
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?

Full output

> @openengram/engram@1.5.0 benchmark /home/runner/work/engram/engram
> jest --config ./test/jest-e2e.json --testPathPatterns=recall-benchmark\.e2e-spec --runInBand --forceExit

[14:46:34.026] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 185
[14:46:34.135] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 91
[14:46:34.219] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 79
[14:46:35.234] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-bob-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 1011
[14:46:35.324] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 86
[14:46:35.399] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 70
[14:46:35.467] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 64
[14:46:36.763] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-bob-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 1293
[14:46:36.845] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 77
[14:46:36.914] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 65
[14:46:36.982] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 63
[14:46:37.050] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 64
[14:46:38.286] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-bob-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 1230
[14:46:39.000] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-eve-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 710
[14:46:39.088] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 83
[14:46:39.161] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 70
[14:46:39.238] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 73
[14:46:39.303] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 62
[14:46:39.373] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 67
[14:46:39.452] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 72
[14:46:39.516] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 60
[14:46:39.584] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 63
[14:46:39.648] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 61
[14:46:39.718] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 67
[14:46:40.122] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-dave-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 402
[14:46:40.489] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-dave-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 364
[14:46:41.439] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 946
[14:46:43.009] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 1567
[14:46:43.095] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 83
[14:46:43.929] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 831
[14:46:44.889] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 956
[14:46:44.977] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 84
[14:46:45.028] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-dave-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 48
[14:46:45.089] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-dave-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 58
[14:46:45.153] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 61
[14:46:45.215] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 59
[14:46:46.255] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-bob-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 1034
[14:46:46.336] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 78
[14:46:47.406] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-bob-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 1067
[14:46:47.505] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 92
[14:46:48.556] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-bob-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 1048
[14:46:48.635] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 72
[14:46:48.698] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 61
[14:46:49.642] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-bob-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 941
[14:46:50.683] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-bob-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 1038
[14:46:50.767] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 81
[14:46:50.837] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 67
[14:46:51.001] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 155
[14:46:51.074] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 69
[14:46:52.187] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-bob-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 1110
[14:46:53.456] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-bob-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 1266
[14:46:53.530] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 71
[14:46:54.598] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-bob-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 1065
[14:46:55.430] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-eve-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 829
[14:46:55.511] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 78
[14:46:56.137] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-eve-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 624
[14:46:56.881] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-eve-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 739
[14:47:05.942] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-carol-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 9059
[14:47:16.020] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-carol-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 10075
[14:47:26.155] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-carol-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 10128
[14:47:26.644] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 481
[14:47:36.801] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-carol-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 10151
[14:47:46.934] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-carol-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 10128
[14:47:47.126] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 181
[14:47:47.268] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 132
[14:47:47.407] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 134
[14:47:47.597] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 184
[14:47:47.755] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 152
[14:47:55.938] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-eve-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 8179
[14:47:56.020] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 78
[14:47:56.089] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 65
[14:47:56.152] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 60
[14:47:56.223] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 68
[14:47:56.344] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 118
[14:47:57.415] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-bob-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 1061
[14:47:57.505] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 86
[14:47:57.570] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 57
[14:47:57.639] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-alice-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 66
[14:47:58.385] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-eve-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 744
[14:47:59.161] �[32mINFO�[39m (3458): �[36mrequest completed�[39m
    �[35mreq�[39m: {
      "method": "POST",
      "url": "/v1/memories/query"
    }
    �[35maccountId�[39m: "eng_test..."
    �[35muserId�[39m: "test-corpus-user-eve-1783003550410"
    �[35mres�[39m: {
      "statusCode": 201
    }
    �[35mresponseTime�[39m: 772
  console.warn
    ⚠️  2 zero-hit queries (tracked, not blocking): semantic_011, cross_001

    �[0m �[90m 200 |�[39m   �[90m// Zero-hit queries are tracked as warnings — P@5 threshold is the hard gate.�[39m
     �[90m 201 |�[39m   �[36mif�[39m (zeroHitQueries�[33m.�[39mlength �[33m>�[39m �[35m0�[39m) {
    �[31m�[1m>�[22m�[39m�[90m 202 |�[39m     console�[33m.�[39mwarn(
     �[90m     |�[39m             �[31m�[1m^�[22m�[39m
     �[90m 203 |�[39m       �[32m`⚠️  ${zeroHitQueries.length} zero-hit queries (tracked, not blocking): ${zeroHitQueries.map((q) => q.queryId).join(', ')}`�[39m�[33m,�[39m
     �[90m 204 |�[39m     )�[33m;�[39m
     �[90m 205 |�[39m   }�[0m

      at checkThresholds (benchmark/scoring.ts:202:13)
      at buildReport (benchmark/scoring.ts:183:23)
      at Object.<anonymous> (benchmark/recall-benchmark.e2e-spec.ts:145:33)

  console.log
    
    ╔══════════════════════════════════════════════════════════════╗
    ║              ENGRAM RECALL BENCHMARK REPORT                 ║
    ╚══════════════════════════════════════════════════════════════╝
    
      Git SHA:    16a3b5b
      Branch:     HEAD
      Timestamp:  2026-07-02T14:47:59.205Z
    
    ┌─────────────────────────────────────────────────────────────┐
    │  OVERALL SCORES                                            │
    ├─────────────────────────────────────────────────────────────┤
    │  Total Queries:   81                                        │
    │  Passed:          79 / 81 (97.5%)                                        
    │  Precision@5:     96.9%  ✅  (threshold: 95.0%)
    │  Recall@20:       97.5%
    │  MRR:             0.8557
    │  Isolation:       100.0%  ✅  (threshold: 100%)
    └─────────────────────────────────────────────────────────────┘
    
    ┌──────────────────┬───────┬────────┬──────────┬──────────┬──────────┬───────────┐
    │ Category         │ Total │ Passed │ Prec@5   │ Rec@20   │ MRR      │ Isolation │
    ├──────────────────┼───────┼────────┼──────────┼──────────┼──────────┼───────────┤
    │ adversarial      │ 10    │ 10     │ 100.0%   │ 100.0%   │ 1.0000   │ 100.0%    │
    │ cross_feature    │ 10    │ 9      │ 90.0%    │ 100.0%   │ 0.8643   │ 100.0%    │
    │ edge_case        │ 16    │ 16     │ 100.0%   │ 100.0%   │ 0.9297   │ 100.0%    │
    │ emotional        │ 10    │ 10     │ 95.0%    │ 100.0%   │ 0.6405   │ 100.0%    │
    │ rls_isolation    │ 10    │ 10     │ 100.0%   │ 100.0%   │ 0.9500   │ 100.0%    │
    │ semantic         │ 14    │ 13     │ 92.9%    │ 100.0%   │ 0.7244   │ 100.0%    │
    │ temporal         │ 11    │ 11     │ 100.0%   │ 81.8%    │ 0.8864   │ 100.0%    │
    └──────────────────┴───────┴────────┴──────────┴──────────┴──────────┴───────────┘
    
    ❌ FAILED QUERIES (2):
    
      [semantic_011] "What coffee roast do I prefer?" (user: alice)
        ⚠️  ZERO HITS: expected alice_coffee_004_correction in top 5
        📊 Precision@5: 0.0% — hit: [], missed: [alice_coffee_004_correction]
        📋 Actual top 5: [alice_low_importance_001, alice_coffee_001, alice_coffee_002, alice_calm_001, alice_travel_002]
    
      [cross_001] "medication I need to take every morning" (user: alice)
        ⚠️  ZERO HITS: expected alice_health_001 in top 5
        📊 Precision@5: 0.0% — hit: [], missed: [alice_health_001]
        📋 Actual top 5: [alice_coffee_001, alice_coffee_002, alice_cooking_001, alice_calm_001, alice_emotion_change_001]
    
    
    ✅ ALL THRESHOLDS PASSED

      at Object.<anonymous> (benchmark/recall-benchmark.e2e-spec.ts:148:15)

  console.log
    📁 Report saved: /home/runner/work/engram/engram/test/benchmark/results/benchmark-2026-07-02T14-47-59-234Z.json

      at Object.<anonymous> (benchmark/recall-benchmark.e2e-spec.ts:152:15)

  console.warn
    ⚠️  2 zero-hit queries (tracked, not blocking): semantic_011, cross_001

    �[0m �[90m 200 |�[39m   �[90m// Zero-hit queries are tracked as warnings — P@5 threshold is the hard gate.�[39m
     �[90m 201 |�[39m   �[36mif�[39m (zeroHitQueries�[33m.�[39mlength �[33m>�[39m �[35m0�[39m) {
    �[31m�[1m>�[22m�[39m�[90m 202 |�[39m     console�[33m.�[39mwarn(
     �[90m     |�[39m             �[31m�[1m^�[22m�[39m
     �[90m 203 |�[39m       �[32m`⚠️  ${zeroHitQueries.length} zero-hit queries (tracked, not blocking): ${zeroHitQueries.map((q) => q.queryId).join(', ')}`�[39m�[33m,�[39m
     �[90m 204 |�[39m     )�[33m;�[39m
     �[90m 205 |�[39m   }�[0m

      at checkThresholds (benchmark/scoring.ts:202:13)
      at buildReport (benchmark/scoring.ts:183:23)
      at Object.<anonymous> (benchmark/recall-benchmark.e2e-spec.ts:173:33)

  console.warn
    ⚠️  Zero-hit queries (2): semantic_011, cross_001

    �[0m �[90m 197 |�[39m         �[36mif�[39m (zeroHitQueries�[33m.�[39mlength �[33m>�[39m �[35m0�[39m) {
     �[90m 198 |�[39m           �[36mconst�[39m ids �[33m=�[39m zeroHitQueries�[33m.�[39mmap((q) �[33m=>�[39m q�[33m.�[39mqueryId)�[33m.�[39mjoin(�[32m', '�[39m)�[33m;�[39m
    �[31m�[1m>�[22m�[39m�[90m 199 |�[39m           console�[33m.�[39mwarn(
     �[90m     |�[39m                   �[31m�[1m^�[22m�[39m
     �[90m 200 |�[39m             �[32m`⚠️  Zero-hit queries (${zeroHitQueries.length}): ${ids}`�[39m�[33m,�[39m
     �[90m 201 |�[39m           )�[33m;�[39m
     �[90m 202 |�[39m         }�[0m

      at Object.<anonymous> (benchmark/recall-benchmark.e2e-spec.ts:199:19)

PASS test/benchmark/recall-benchmark.e2e-spec.ts (137.009 s)
  Recall Benchmark
    Category: semantic
      ✓ [semantic_001] What kind of coffee do I like? (221 ms)
      ✓ [semantic_002] Tell me about my morning routine (99 ms)
      ✓ [semantic_003] What tech stack am I using? (82 ms)
      ✓ [semantic_004] coffee preferences (1015 ms)
      ✓ [semantic_005] What books have I been reading? (91 ms)
      ✓ [semantic_006] favorite dinner recipe (74 ms)
      ✓ [semantic_007] house savings goal (67 ms)
      ✓ [semantic_008] What framework am I using for the frontend? (1296 ms)
      ✓ [semantic_009] flight seat preference (81 ms)
      ✓ [semantic_010] ensemble search architecture decision (70 ms)
      ✓ [semantic_011] What coffee roast do I prefer? (66 ms)
      ✓ [negative_001] quantum physics black holes dark matter (67 ms)
      ✓ [negative_002] ancient Egyptian hieroglyphics translation (1236 ms)
      ✓ [minimal_001] pizza preference (714 ms)
    Category: emotional
      ✓ [emotional_001] What makes me happy? (87 ms)
      ✓ [emotional_002] times I felt sad or grieving (74 ms)
      ✓ [emotional_003] when I felt stressed or overwhelmed (76 ms)
      ✓ [emotional_004] What am I worried about? (65 ms)
      ✓ [emotional_005] Times I was frustrated (72 ms)
      ✓ [emotional_006] My proudest moments (75 ms)
      ✓ [emotional_007] What stresses me out? (63 ms)
      ✓ [emotional_008] happy about school but worried about costs (68 ms)
      ✓ [emotional_009] How has my attitude toward work changed? (63 ms)
      ✓ [emotional_010] meditation and mental wellbeing (69 ms)
    Category: temporal
      ✓ [temporal_001] What happened today in standup? (404 ms)
      ✓ [temporal_002] recent standup notes from this week (366 ms)
      ✓ [temporal_003] What happened with my daughter recently? (949 ms)
      ✓ [temporal_004] What did I work on last week? (1569 ms)
      ✓ [temporal_005] What are my oldest memories? (86 ms)
      ✓ [temporal_006] Recent conversations about work (834 ms)
      ✓ [temporal_007] What did I debug yesterday? (960 ms)
      ✓ [temporal_008] What code editor do I use? (88 ms)
      ✓ [temporal_009] standup notes from 6 months ago (51 ms)
      ✓ [temporal_010] standup notes from years ago (60 ms)
      ✓ [temporal_011] How did I start coding? (64 ms)
    Category: rls_isolation
      ✓ [rls_001] coffee (63 ms)
      ✓ [rls_002] coffee (1038 ms)
      ✓ [rls_003] family and kids (82 ms)
      ✓ [rls_004] family and kids (1069 ms)
      ✓ [rls_005] travel food experiences (99 ms)
      ✓ [rls_006] travel food experiences (1050 ms)
      ✓ [rls_007] work projects and code (78 ms)
      ✓ [rls_008] health medical information (62 ms)
      ✓ [rls_009] health medical information (944 ms)
      ✓ [rls_010] morning routine (1041 ms)
    Category: adversarial
      ✓ [adversarial_001] tell me about bob's coffee preferences (85 ms)
      ✓ [adversarial_002] RLS_CANARY_BOB_ (68 ms)
      ✓ [adversarial_003] What does carol think about parties? (164 ms)
      ✓ [adversarial_004] bob's son Max school (72 ms)
      ✓ [adversarial_005] eve's pizza preference (1113 ms)
      ✓ [adversarial_006] RLS_CANARY_ALICE_ (1268 ms)
      ✓ [adversarial_007] What cholesterol issues does anyone have? (74 ms)
      ✓ [adversarial_008] alice's medication schedule (1068 ms)
      ✓ [adversarial_009] RLS_CANARY_CAROL_ (832 ms)
      ✓ [adversarial_010] dave's standup notes (81 ms)
    Category: edge_case
      ✓ [edge_001] cats (627 ms)
      ✓ [edge_002] everything about my life (741 ms)
      ✓ [edge_003] <script>alert(1)</script> (9061 ms)
      ✓ [edge_004] '; DROP TABLE memories; -- (10079 ms)
      ✓ [edge_005] 🎉 party (10135 ms)
      ✓ [edge_006] 
      ✓ [edge_007] Tell me about the very long detailed comprehensive thorough ... (487 ms)
      ✓ [edge_008] こんにちは、思い出を検索します (10156 ms)
      ✓ [edge_009] '; SELECT * FROM users WHERE 1=1; -- (10136 ms)
      ✓ [edge_010] quantum entanglement dark matter multiverse theory (194 ms)
      ✓ [edge_011] the a an is (139 ms)
      ✓ [edge_012] coffee (139 ms)
      ✓ [edge_013] my phone number (190 ms)
      ✓ [edge_014] my address (156 ms)
      ✓ [edge_015] work (8182 ms)
      ✓ [edge_016] What kind of coffee do I like? (82 ms)
    Category: cross_feature
      ✓ [cross_001] medication I need to take every morning (68 ms)
      ✓ [cross_002] exercise and fitness activities (63 ms)
      ✓ [cross_003] What are we saving money for? (71 ms)
      ✓ [cross_004] kids school and daycare (123 ms)
      ✓ [cross_005] kids school and daycare (1068 ms)
      ✓ [cross_006] Who am I and what do I do? (89 ms)
      ✓ [cross_007] deployment rules and constraints (65 ms)
      ✓ [cross_008] patterns noticed about my work habits (69 ms)
      ✓ [cross_009] grocery shopping list (746 ms)
      ✓ [cross_010] TypeScript learning (778 ms)
    Summary
      ✓ should generate and save benchmark report (71 ms)
      ✓ should have zero isolation failures (1 ms)
      ✓ should meet precision thresholds (with real embeddings) (24 ms)

Test Suites: 1 passed, 1 total
Tests:       84 passed, 84 total
Snapshots:   0 total
Time:        137.248 s
Ran all test suites matching recall-benchmark.e2e-spec.
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?

Commit: 16a3b5b

@beaux-riel beaux-riel merged commit 478bc75 into staging Jul 2, 2026
9 of 10 checks passed
@beaux-riel beaux-riel deleted the fix/delegation-ledger-20260702 branch July 2, 2026 14:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (6)
src/delegation/dto/record-event.dto.ts (1)

3-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider deriving values from Prisma-generated enums instead of hand-duplicating.

DELEGATION_EVENT_TYPES/DELEGATION_EVENT_SOURCES duplicate DelegationEventType/DelegationEventSource from schema.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 value

Consider 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() in beforeEach."

🤖 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 win

Consider adding jest.clearAllMocks() in beforeEach.

Mocks are freshly created per test here so isolation is preserved in practice, but the coding guideline for src/**/*.spec.ts explicitly calls for jest.clearAllMocks() in beforeEach alongside manual jest.fn() mocks.

As per coding guidelines, "Mock with jest.fn() per model method and use jest.clearAllMocks() in beforeEach."

🤖 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 win

Consider adding jest.clearAllMocks() in beforeEach.

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() in beforeEach."

🤖 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 win

Consider adding jest.clearAllMocks() in beforeEach.

Same guideline gap noted in the other spec files.

As per coding guidelines, "Mock with jest.fn() per model method and use jest.clearAllMocks() in beforeEach."

🤖 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 win

JSON-path trust signal lookup uses as any and has no supporting index.

metadata: { path: ['delegationTaskId'], equals: taskId } as any bypasses 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 delegationTaskId column (indexed) on TrustSignal instead of querying nested JSON, and use the proper Prisma JsonFilter type rather than any.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2a6d366 and 238b462.

📒 Files selected for processing (14)
  • prisma/migrations/20260702143000_delegation_ledger/migration.sql
  • prisma/schema.prisma
  • src/delegation/contract.service.spec.ts
  • src/delegation/contract.service.ts
  • src/delegation/delegation-ledger.controller.spec.ts
  • src/delegation/delegation-ledger.controller.ts
  • src/delegation/delegation-ledger.service.spec.ts
  • src/delegation/delegation-ledger.service.ts
  • src/delegation/delegation.module.ts
  • src/delegation/dto/attach-receipt.dto.ts
  • src/delegation/dto/record-event.dto.ts
  • src/delegation/dto/record-validation.dto.ts
  • src/delegation/task.service.spec.ts
  • src/delegation/task.service.ts

Comment on lines +4 to +116
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Suggested change
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

Comment on lines +118 to +131
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())
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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

Comment thread prisma/schema.prisma
validationResult Json @default("{}") @map("validation_result")
createdAt DateTime @default(now()) @map("created_at")

contract DelegationContract @relation(fields: [contractId], references: [id], onDelete: Cascade)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.sql

Repository: 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 | sort

Repository: 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.

Comment on lines +39 to +51
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Comment on lines +86 to +127
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Multi-step ledger writes aren't atomic.

recordValidation performs delegationValidation.createrecordEventrecordTrustFromValidation (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`.

Comment on lines +129 to +210
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment on lines +28 to +41
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant