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 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-03-03 09:32:04 +00:00
parent daa11bab39
commit 627813cc4d
2 changed files with 245 additions and 0 deletions

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Create brain_memories table for OpenBrain shared knowledge store.
*
* Guarded with hasTable() so this migration is idempotent and
* can coexist with the consolidated app-level migration.
*/
public function up(): void
{
Schema::disableForeignKeyConstraints();
if (! Schema::hasTable('brain_memories')) {
Schema::create('brain_memories', function (Blueprint $table) {
$table->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();
}
};

188
Models/BrainMemory.php Normal file
View file

@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
namespace Core\Mod\Agentic\Models;
use Core\Tenant\Concerns\BelongsToWorkspace;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Brain Memory - a unit of shared knowledge in the OpenBrain store.
*
* Agents write observations, decisions, conventions, and research
* into the brain so that other agents (and future sessions) can
* recall organisational knowledge without re-discovering it.
*
* @property string $id
* @property int $workspace_id
* @property string $agent_id
* @property string $type
* @property string $content
* @property array|null $tags
* @property string|null $project
* @property float $confidence
* @property string|null $supersedes_id
* @property \Carbon\Carbon|null $expires_at
* @property \Carbon\Carbon|null $created_at
* @property \Carbon\Carbon|null $updated_at
* @property \Carbon\Carbon|null $deleted_at
*/
class BrainMemory extends Model
{
use BelongsToWorkspace;
use HasUuids;
use SoftDeletes;
/** Valid memory types. */
public const VALID_TYPES = [
'decision',
'observation',
'convention',
'research',
'plan',
'bug',
'architecture',
];
protected $table = 'brain_memories';
protected $fillable = [
'workspace_id',
'agent_id',
'type',
'content',
'tags',
'project',
'confidence',
'supersedes_id',
'expires_at',
];
protected $casts = [
'tags' => '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(),
];
}
}