Critical fixes:
- Quota enforcement regressed when the regex flow was replaced with
tools. The LLM was trusted to respect [Session state] quota hints,
which is vulnerable to prompt injection / hallucination. Each tool
now checks Pennant feature limits at the top of handle() and returns
a short quota-exhausted string instead of calling the provider.
Audio shares the video quota (there is no AiAudioLimit by design).
- N+1 on aiMessages: combined the two separate ->get() scans that
produced imagesInThread and videosInThread into a single query.
- postPlatforms lazy load: controller now loadMissing('postPlatforms')
before constructing SocialMediaAssistant, which accesses the relation
in activePlatformRules().
Project conventions:
- \RuntimeException and \Throwable are now imported at the top of the
controller instead of inlined per CLAUDE.md.
- Tool handle() methods use data_get($request, 'prompt') instead of
direct array access, matching the data_get convention for service
classes.
- enrichContent() pluralizes attachment types via Str::plural so
'2 images' reads naturally instead of '2 image'.
Tests:
- Added Storage::fake('public') in tool tests so runs don't pollute
the local disk with fake-generated files.
- Restored quota enforcement coverage (deleted when the flow changed)
as three new tests — one per tool — that fill the monthly bucket
and assert the tool refuses and nothing is generated.
- Removed inline \App\ references in tests in favor of imports.
Controller now delegates to SocialMediaAssistant::prompt() and reads
generated attachments from AttachmentCollector (request-scoped). The
three preg_match branches for [GENERATE_IMAGE/VIDEO/AUDIO] commands
are gone — the LLM invokes tools directly with typed parameters.
Deleted:
- app/Services/Ai/GeminiTextGenerationService.php
- app/Services/Ai/TextGenerationService.php (OpenAI alternative)
- app/Services/Ai/ImageGenerationService.php
- app/Services/Ai/AudioGenerationService.php
- app/Services/Ai/Contracts/TextGenerationInterface.php
Kept: VideoGenerationService (wrapped by GenerateVideo tool since
Veo is not in the SDK's provider matrix) and IntentDetector.
Tests now fake the agent via SocialMediaAssistant::fake() with either
canned text or a callable that simulates tool side-effects by pushing
directly into AttachmentCollector.