From 627813cc4d0aed95aaf1b71373e262b14903c27c Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 3 Mar 2026 09:32:04 +0000 Subject: [PATCH] feat(brain): add BrainMemory model and migration UUID-keyed brain_memories table with workspace scoping, self-referential supersession chain, TTL expiry, and confidence scoring. Eloquent model includes all scopes and helpers needed by the MCP tool layer. Co-Authored-By: Claude Opus 4.6 --- ..._01_000008_create_brain_memories_table.php | 57 ++++++ Models/BrainMemory.php | 188 ++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 Migrations/0001_01_01_000008_create_brain_memories_table.php create mode 100644 Models/BrainMemory.php diff --git a/Migrations/0001_01_01_000008_create_brain_memories_table.php b/Migrations/0001_01_01_000008_create_brain_memories_table.php new file mode 100644 index 0000000..5cb976f --- /dev/null +++ b/Migrations/0001_01_01_000008_create_brain_memories_table.php @@ -0,0 +1,57 @@ +uuid('id')->primary(); + $table->foreignId('workspace_id')->constrained()->cascadeOnDelete(); + $table->string('agent_id', 64); + $table->string('type', 32)->index(); + $table->text('content'); + $table->json('tags')->nullable(); + $table->string('project', 128)->nullable()->index(); + $table->float('confidence')->default(1.0); + $table->uuid('supersedes_id')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index('workspace_id'); + $table->index('agent_id'); + $table->index(['workspace_id', 'type']); + $table->index(['workspace_id', 'project']); + + $table->foreign('supersedes_id') + ->references('id') + ->on('brain_memories') + ->nullOnDelete(); + }); + } + + Schema::enableForeignKeyConstraints(); + } + + public function down(): void + { + Schema::disableForeignKeyConstraints(); + Schema::dropIfExists('brain_memories'); + Schema::enableForeignKeyConstraints(); + } +}; diff --git a/Models/BrainMemory.php b/Models/BrainMemory.php new file mode 100644 index 0000000..c7b7e0e --- /dev/null +++ b/Models/BrainMemory.php @@ -0,0 +1,188 @@ + 'array', + 'confidence' => 'float', + 'expires_at' => 'datetime', + ]; + + // ---------------------------------------------------------------- + // Relationships + // ---------------------------------------------------------------- + + public function workspace(): BelongsTo + { + return $this->belongsTo(Workspace::class); + } + + /** The older memory this one replaces. */ + public function supersedes(): BelongsTo + { + return $this->belongsTo(self::class, 'supersedes_id'); + } + + /** Newer memories that replaced this one. */ + public function supersededBy(): HasMany + { + return $this->hasMany(self::class, 'supersedes_id'); + } + + // ---------------------------------------------------------------- + // Scopes + // ---------------------------------------------------------------- + + public function scopeForWorkspace(Builder $query, int $workspaceId): Builder + { + return $query->where('workspace_id', $workspaceId); + } + + public function scopeOfType(Builder $query, string|array $type): Builder + { + return is_array($type) + ? $query->whereIn('type', $type) + : $query->where('type', $type); + } + + public function scopeForProject(Builder $query, ?string $project): Builder + { + return $project + ? $query->where('project', $project) + : $query; + } + + public function scopeByAgent(Builder $query, ?string $agentId): Builder + { + return $agentId + ? $query->where('agent_id', $agentId) + : $query; + } + + /** Exclude memories whose TTL has passed. */ + public function scopeActive(Builder $query): Builder + { + return $query->where(function (Builder $q) { + $q->whereNull('expires_at') + ->orWhere('expires_at', '>', now()); + }); + } + + /** Exclude memories that have been superseded by a newer version. */ + public function scopeLatestVersions(Builder $query): Builder + { + return $query->whereDoesntHave('supersededBy', function (Builder $q) { + $q->whereNull('deleted_at'); + }); + } + + // ---------------------------------------------------------------- + // Helpers + // ---------------------------------------------------------------- + + /** + * Walk the supersession chain and return its depth. + * + * A memory that supersedes nothing returns 0. + * Capped at 50 to prevent runaway loops. + */ + public function getSupersessionDepth(): int + { + $depth = 0; + $current = $this; + $maxDepth = 50; + + while ($current->supersedes_id !== null && $depth < $maxDepth) { + $current = $current->supersedes; + + if ($current === null) { + break; + } + + $depth++; + } + + return $depth; + } + + /** Format the memory for MCP tool responses. */ + public function toMcpContext(): array + { + return [ + 'id' => $this->id, + 'agent_id' => $this->agent_id, + 'type' => $this->type, + 'content' => $this->content, + 'tags' => $this->tags, + 'project' => $this->project, + 'confidence' => $this->confidence, + 'supersedes_id' => $this->supersedes_id, + 'expires_at' => $this->expires_at?->toIso8601String(), + 'created_at' => $this->created_at?->toIso8601String(), + 'updated_at' => $this->updated_at?->toIso8601String(), + ]; + } +}