'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(), ]; } }