30 Commits
Author SHA1 Message Date
James Wampler ab773615f8 Serve the unified tool registry over MCP Streamable HTTP at /mcp
Adds ModelContextProtocol.AspNetCore and registers AddMcpServer with
WithListToolsHandler/WithCallToolHandler rather than 45 attribute
methods, so both handlers resolve the scoped NovelAgentToolset per
request and reuse its hand-built schemas directly instead of fighting
the SDK's delegate-based schema inference.

NovelMcpTools (src/Novelly.Api/Mcp/) is the adapter: it injects a
required novelId property into the advertised schema for tools that
need one and extracts it back out at call time, since the web agent
gets novelId ambiently from its route but an MCP client has no route
to supply it from.

/mcp inherits auth from the existing fallback policy (cookie or
X-Novelly-Api-Key) by adding no authorization metadata of its own —
chaining .RequireAuthorization() would apply the default,
cookie-only policy instead and break the API key. Verified end to end
against a running instance: initialize advertises capabilities.tools,
tools/list returns all 45 with novelId injected only where needed,
tool errors map to result.isError rather than a JSON-RPC error, and a
write (create_tag) round-trips correctly with the service user's
identity intact.
2026-08-21 10:56:24 -07:00
James Wampler 897fb442a1 Unify MCP and agent tool surfaces onto one registry in NovelAgentToolset
Fixes NovelAgentService continuing a conversation under the wrong
novel's route, since FindConversationAsync matched by id alone. Then
extends NovelAgentToolset to all 45 tools the stdio MCP server offered
(tag/location CRUD, character relationships, arc-stage beat pinning,
question editing, cross-novel novel listing/creation), tagging each
with whether it needs an explicit novel scope so a later MCP adapter
can inject it. Renames the toolset's 33 existing schemas from
snake_case to camelCase to match .NET/REST convention, since nothing
external consumes them.

Lays the groundwork to serve this same registry over MCP at /mcp and
retire the separate stdio Novelly.Mcp project (docs/plans/api/mcp_http_merge_plan.md).
2026-08-21 10:52:50 -07:00
James Wampler aca26588f9 Add soft delete + trash, keyboard-first web overhaul, move chapter tags to bottom
CI / build-and-push (push) Failing after 31s
CI / deploy (push) Has been skipped
Adds SoftDelete/Trash across characters, chapters, locations, beats with a
purge schedule and Trash page. Reworks the web client for keyboard-driven
navigation (focus helpers, help overlay, keyboard.md doc). Moves the
ChapterPage tag editor to the bottom of the page to match CharacterDetailPage.
2026-08-20 16:39:09 -07:00
James Wampler 661f2917ea Add zip upload and mapped-path picker for outline import
Sandbox source paths under a configured Imports:RootPath, browse it
from the web dialog, upload a zip that extracts into staging, and
import a single markdown file (agent infers chapter vs character).
2026-08-20 14:21:44 -07:00
James Wampler 7f56c79b20 Label chapter chips by kind, not raw number
Beats, tags, locations, open questions, and character/arc-stage
responses that reference a chapter now carry a ChapterLabel/DisplayNumber
alongside the raw Number, computed via the new
ChapterDisplayNumberLookup. Front/back matter chips show their title;
body chapters show "Chapter N: Title".
2026-08-19 18:02:53 -07:00
James Wampler ef5260a111 Add ChapterKind for front/back matter chapters
Chapters can now be marked FrontMatter/Body/BackMatter. Number stays
the manuscript sort key for every chapter; the author-facing display
number is now computed per-request as the chapter's ordinal among
Body chapters only, so a foreword or afterword no longer shifts the
numbering of the rest of the book. Surfaced through the API, agent
toolset, and import toolset.
2026-08-19 17:54:20 -07:00
James Wampler 71953220aa Add GitHub-style activity contribution graph
CI / build-and-push (push) Successful in 52s
CI / deploy (push) Successful in 9s
Record create/update/delete events across novel content (chapters, beats,
characters, arc stages, tags, locations, questions) into an append-only
ActivityEvent log, aggregate by UTC day, and surface as a heatmap on the
novels list and each novel's dashboard. Backfills history from existing
CreatedAt timestamps on first boot after the migration.
2026-08-19 17:39:13 -07:00
James Wampler 45afc980d3 Simplify character dossier fields, add ShowPronouns setting
CI / build-and-push (push) Successful in 51s
CI / deploy (push) Successful in 9s
Collapse Want/Need into Motivation and Internal/External Conflict into
Conflict, drop Arc summary field, and move tags below open questions on
the character page. Gate Pronouns display behind new UiSettings:ShowPronouns
config (default off).
2026-08-19 15:33:14 -07:00
James Wampler c620ddd626 Replace chapter setting with multi-select locations; add chapter character summary
Chapters now carry many Locations (new Tags-style entity with cross-referencing)
instead of a single free-text Setting field, with a Locations tab on the novel
for browsing them and seeing every chapter set at each one. Also surfaces the
distinct characters appearing in a chapter's beats, linked, under the beat/word
count on the outline tab.
2026-08-18 11:36:13 -07:00
James Wampler 4313c8f206 Rename Project concept to Novel across the stack
Renames the domain concept from Project to Novel throughout the backend
(entities, DTOs, services, endpoints, ProjectAccessService/Permission,
ProjectId foreign keys), MCP server (tool names and routes), and the
React/Vite frontend (types, hooks, routes, components). Adds a new EF
Core migration (RenameProjectToNovel) using RenameTable/RenameColumn to
preserve existing data instead of dropping/recreating tables. Updates
CLAUDE.md's structure section to reference Novels/ instead of Projects/.
2026-08-17 23:03:09 -07:00
James Wampler bc85a36e18 Dashboard: show 10 chapters titled Chapters; bump chapter mtime on beat changes
Beat create/update/delete/reorder/assign/move now touch the parent
chapter's UpdatedAt so the dashboard's recency sort reflects beat edits,
not just chapter-level prose/tag changes.
2026-08-17 22:53:05 -07:00
James Wampler 17facba3b9 Group character beats into arc-stage sections; reciprocal relationship types
Arc stages now group the beats that establish or pay off that stage of a
character's arc (many-to-many via ArcStageBeats), and carry a Result field
(renamed from Description) describing what the stage results in for the
character. Assigning a beat to a stage moves it out of any other stage of
the same character. New endpoint POST /api/arc-stages/{id}/beats, MCP tool
set_arc_stage_beats, and frontend grouping UI in CharacterArc/CharacterBeats.

Also records a relationship's reciprocal type so both characters' dossiers
show the correct direction (e.g. "sister" / "brother") instead of mirroring
the same label.
2026-08-17 22:45:07 -07:00
James Wampler f124b9b4bb Add character aliases/identity links, move-beats, keyboard help overlay
- Characters can carry aliases and be linked as the same underlying
  person (canonical SameCharacterAsId, optional reveal chapter/note),
  surfaced through the API, MCP tools, agent toolset, and web UI.
- Characters page redesigned as a filterable/sortable table (name+
  aliases, role, importance, occupation, tags) instead of a sidebar
  list, to stay usable as the cast grows.
- Beats can be moved between chapters (BeatService.MoveAsync + MCP/
  agent tool + endpoint).
- Add a keyboard-shortcuts help overlay (HelpButton/HelpOverlayContext)
  wired into the project layout.
- CLAUDE.md: require every frontend component to carry a unique id
  attribute; apply it to CharacterMultiSelect and MarkdownEditor.
2026-08-17 17:26:50 -07:00
Claude e2a2e69631 Surface project ownership/role on the wire and gate the web UI by it
ProjectResponse now carries OwnerId and a server-resolved MyRole so
Editors/Reviewers see read-only fields and no delete/grant-management
affordances instead of only finding out via a 403 after the fact.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PuBH9QSv66DPXSSBERmPs6
2026-08-16 15:37:50 +00:00
James Wampler e598c18d67 Add users, roles, and per-novel permissions
Introduces accounts (ASP.NET Identity + cookie auth), four global
roles (Admin/Writer/Editor/Reviewer), per-novel ownership and grants
via ProjectMember, and a service-API-key principal for the MCP server
and background import jobs. Enforcement lives in the application
services (not endpoint filters) so the embedded agent and MCP tools,
which call the same services directly, can't bypass it. Web client
gets a login page, session-aware routing, and a People section for
managing per-novel access.

Also includes prior in-flight changes from this branch (CLAUDE.md
compliance pass, dev-deploy docker-compose setup) that were
uncommitted when this feature work started.
2026-08-15 22:29:33 -07:00
James Wampler df10b1f99b Add batch character assignment for chapter outline beats
Lets a writer select several beats and add one character to all of them
at once, without disturbing each beat's existing characters. Surfaced
through the REST API, the embedded agent, and the MCP server per the
project's rule that all three share the same service methods.
2026-08-15 20:36:25 -07:00
James Wampler 27c287b9c8 Add genre feature; extract ConfirmModal for reusable confirm dialogs
Genres seed on boot, are editable per project, and gate agent config
errors more gracefully. ConfirmModal replaces ad-hoc confirm prompts
across chapters, tags, and characters pages.
2026-08-15 11:25:13 -07:00
James Wampler 23348327a9 Remove Scenes, group beats by multiple characters; strip comments repo-wide
Drop the Scene entity/grouping in favor of chapters carrying prose directly
and beats belonging to many characters. Add markdown editor + character
multi-select components to the web client. Remove all XML doc and inline
comments across the touched C#/TS/CSS files in favor of self-documenting
names, and record that convention in CLAUDE.md. Add .mcp.json (local MCP
server config, no secrets) and ignore .idea/.
2026-08-11 21:05:13 -07:00
James Wampler e7d89a6d41 Add novel dashboard with project phases; fix beat/chapter overflow
Dashboard replaces the tabbed overview: Outline + Characters cards
(recent-first), gated by a new Project.Phase (Brainstorming ->
Outlining -> Writing -> Editing -> Complete). Brainstorming shows just
a notes editor; existing projects backfill to Outlining. Old brief
editor moves to a Settings route.

Beat table now renders as plain readable text, turning into editable
fields only for the clicked row. Fixed horizontal overflow on the
chapters list and beat table caused by unconstrained CSS grid/table
tracks sizing to their widest unwrapped child. Widened the project
layout's max width.
2026-08-10 22:46:09 -07:00
James Wampler 2ccebb31eb Remove NotFoundException; services return null on lookup miss
A missing record isn't exceptional — services now return null (logged
at Info) instead of throwing, and endpoints map null to 404. Agent and
import toolsets route not-found through their existing OrNotFound
result pattern rather than a caught exception.
2026-08-06 21:22:40 -07:00
James Wampler 189ebf3237 Add outline import feature; drop Dto naming, map entities at the API boundary
Services now return entities; endpoints (and the agent toolsets) map to
*Response records instead of services building wire DTOs themselves.
Also brings in the outline-import agent, MCP tool, ledger and web dialog
that were already in progress on disk.
2026-08-06 18:36:40 -07:00
James Wampler 40f93e40a8 Stop throwing for not-found; add Guard and request validation
Not-found lookups return null/false instead of throwing NotFoundException
across all services — a missing row is expected control flow, not an
exceptional condition. NotFoundException stays for embedded precondition
checks inside mutations (missing parent, invalid foreign reference).

Guard (copied from mic-check) enforces required arguments at the top of
every service method. A ported IModelValidator<T> framework validates
every request DTO at the API layer via a new ValidationEndpointFilter,
returning a 400 with field-level messages; services re-run the same
validator and throw for direct callers that bypass the API.

Endpoints translate null/false into 404 via a new ToApiResult() helper.
The agent toolset boundary translates the same nullable/bool results
into the tool-error text the model already expected.
2026-08-06 15:13:36 -07:00
James Wampler 04917fa09e Add Serilog console logging across the API
Information at endpoint and service-method boundaries, Debug in deeper
helpers, Warning before expected/recoverable failures (not-found,
validation, agent tool errors), Error on caught exceptions. Serilog
wraps the exception handler so request-completion logs report the
resolved status code rather than the raw exception. Never logs prose
bodies or the Anthropic API key.
2026-08-06 12:11:20 -07:00
James WamplerandClaude Opus 5 4f396bb5f9 Surface arcs, character beats and open questions in the web client
The character page gains three sections under the dossier: the arc as an
editable ordered table with each stage pinnable to a chapter, every beat the
character appears in across the book (each row linking into that chapter's
outline), and the character's open questions. The sidebar groups main
characters above supporting ones, and both the sheet and the add dialog let you
set importance.

The arc section shows for main characters, and also for supporting ones that
already have stages — demoting someone should not hide work they thought they
had lost.

The outline page gains a notes section and an open-questions section at the
bottom. Beat rows are now anchored so the character page can link straight to a
row. Raising a question from either page attaches it to what that page is
about, and the section hides the association it is already scoped to rather
than repeating "Landfall" on every row.

Also fixes an ordering wart the browser run exposed: both CharacterRole and
CharacterImportance are stored as text, so ordering them in SQL ordered the
spelling — "Deuteragonist" beat "Protagonist" and the sidebar put the second
lead above the character the book is about. Listing now sorts after
materialising, which uses the enums' declaration order. The test for it was
checked both ways: it fails on the SQL ordering and passes on the fix.

73 tests pass, the web client builds and lints clean. Driven in a browser
end to end: resolving a question with "also add to notes" drops it off the open
list and appends the decision under the chapter's existing note, "show
resolved" brings it back with a Reopen button, and a beat link on the character
page lands on the right chapter outline at that beat's anchor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
2026-08-06 12:11:20 -07:00
James WamplerandClaude Opus 5 0358667679 Add main/supporting characters, character arcs and open questions
Three things the outline could not express before:

Main vs supporting. A new CharacterImportance sits alongside CharacterRole
rather than inside it — role is the part a character plays (protagonist,
mentor, foil), importance is how much of the book they carry, and a mentor can
be either. Characters start Supporting and get promoted. Listings put main
characters first.

Character arcs. A main character's arc is a flat ordered list of stages, the
same shape as a chapter's beats and for the same reason: an arc is a sequence
of changes, not a tree. A stage can be pinned to the chapter where it lands.
Nothing refuses an arc on a supporting character — demoting someone should not
delete their work.

Open questions. What the writer has not decided yet, hanging off a chapter
outline, a character, both, or neither. They can be resolved, reopened or
deleted, and resolving can append the decision to the notes of whatever the
question was attached to, so it lands where the writer will re-read it.
Resolved questions drop off the list unless asked for.

Also adds GET /api/characters/{id}/beats — every beat a character appears in,
in manuscript order, carrying each beat's chapter so the character page can
link straight into that chapter's outline.

Deletes are deliberately asymmetric: deleting a chapter unpins arc stages and
detaches questions rather than taking them, because a plan outlives a decision
about where the chapter break falls. Deleting a character or project does take
their arcs and questions.

All three capabilities are surfaced in the REST API, the agent toolset and the
MCP server, per the one-source-of-truth rule.

Two things worth flagging in the migration: EF's generated default for the new
Importance column was an empty string, which does not parse back to a
CharacterImportance and would have faulted every read of an existing dossier —
it now defaults to Supporting, verified by migrating a database seeded on the
old schema and reading the row back through the API. And the earlier migrations
were renamed to the namespace EF derives from the output folder, so future
`migrations add` runs stop drifting.

72 tests pass (28 new). The endpoints were also exercised over curl end to end:
arc stages resolving their chapter, a character's beats across chapters, and a
question attached to both a chapter and a character resolving into both sets of
notes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
2026-08-06 12:11:20 -07:00
James WamplerandClaude Opus 5 725758ccd9 Reorganise by feature, rename to Novelly, add Aspire and a pre-push hook
The layered split into Domain/Application/Infrastructure/Api was forcing
organisation by layer: adding one capability meant touching four projects and
four folders that each held a slice of it. Those four projects are now one
feature-organised Novelly.Api, where each folder — Projects, Characters,
Chapters, Beats, Scenes, Tags, Agent — holds its entity, DTOs, service and
endpoints together. Common/ holds what genuinely crosses features (the patch
semantics, the two exception types, DraftStatus) and Data/ holds the DbContext
and migrations.

Six .NET projects become five: the three layer projects are gone, and
Novelly.AppHost and Novelly.ServiceDefaults are new.

- Namespaces move from NovelSoftware.* to Novelly.*, including the entity type
  names recorded in the EF model snapshots. The migration ids are untouched, so
  an existing novel.db still migrates cleanly — verified against a fresh file.
- Aspire orchestration mirrors the mic-check setup: the AppHost starts the API
  on :5080 and the Vite dev server on :5173, and the API picks up OpenTelemetry,
  health checks and service discovery from ServiceDefaults. /health and /alive
  now answer in development.
- A Husky pre-push hook runs scripts/ci/prepush.sh: build, test, then a web
  build. The scripts are plain bash so CI can run the same steps.
- The MCP server's env var is now NOVELLY_API_URL.

Verified beyond the build: 44 tests pass, the web client builds, the API was
exercised over curl (project/chapter/beat/tag round trip, tag cross-reference,
503 on the agent without a key while conversation listing still returns 200),
the MCP server was driven over stdio JSON-RPC (26 tools, errors still surface
the API's own message rather than being flattened), and the AppHost was run to
confirm both resources come up and Vite proxies /api through to the API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
2026-08-06 12:11:20 -07:00
James WamplerandClaude Opus 5 30e0c6926e Migrate the test suite from xUnit + FluentAssertions to NUnit
All 44 tests, same names, same coverage — verified by diffing the runner's test
list before and after.

The load-bearing change is fixture lifecycle. xUnit builds a new test-class
instance per test, so a `readonly TestDatabase _db = new()` field gave every
test its own database. NUnit reuses one instance for the whole class, so those
field initialisers and constructors would have shared a single database across a
class and let tests read each other's rows. Setup moved into [SetUp]/[TearDown]
via a new ServiceTestFixture base class, which also collapses the per-class
service wiring that was duplicated five times.

Assertions are now Assert.That with the constraint model:

  Should().Be(x)              -> Is.EqualTo(x)
  Should().BeNull()           -> Is.Null
  Should().HaveCount(n)       -> Has.Count.EqualTo(n)
  Should().Equal(a, b)        -> Is.EqualTo(new[] { a, b })
  Should().BeEquivalentTo(..) -> Is.EquivalentTo(..)
  Should().Contain("x")       -> Does.Contain("x")
  Should().OnlyHaveUniqueItems() -> Is.Unique
  ThrowAsync<T>().WithMessage("*m*")
      -> Throws.TypeOf<T>().With.Message.Contains("m")

FluentAssertions' `.Which` chains became plain indexed asserts, grouped in
Assert.Multiple so a failure reports every broken expectation in the case rather
than stopping at the first.

Because a framework migration can quietly produce vacuously-passing tests,
spot-checked four conversions by mutation — breaking the code under an async
Assert.Multiple block, a sync one, a Throws constraint, and a collection
ordering assert. All four failed as they should, confirming the assertions are
live and that NUnit bound the async lambdas to AsyncTestDelegate rather than
silently accepting them as async void.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
2026-08-06 12:11:20 -07:00
James WamplerandClaude Opus 5 1852ceb2d1 Adopt the mic-check CLAUDE.md and .editorconfig house standards
Ported both files from wamplerj/mic-check and retargeted them to this project's
stack, then brought the code into line with the rules rather than watering the
rules down to fit the code.

.editorconfig — C# rules carried over verbatim, with four changes:

- Added root = true and a [*] section (utf-8, space indent, final newline,
  trim trailing whitespace). Without root the file inherits from any parent
  .editorconfig above the checkout.
- end_of_line lf rather than crlf. Every file here is LF and there is no
  .gitattributes to normalise on checkout, so crlf would rewrite the tree on
  first save.
- csharp_style_namespace_declarations file_scoped, was block_scoped. The source
  file sets file_scoped under [*.{cs,vb}] and block_scoped under [*.cs]; the
  C#-specific key wins, so the two disagreeing meant C# silently got
  block_scoped. Every .cs file here is file-scoped.
- Added sections for the React client (ts/tsx/js 2-space, 100 cols), json/yaml,
  css/html, markdown (trailing whitespace preserved — it is a line break there)
  and MSBuild files.

Also dropped a duplicated dotnet_naming_style.pascal_case block that appeared
twice verbatim in the source.

CLAUDE.md — same structure and voice, retargeted: React not Vue, xUnit and
FluentAssertions not NUnit and jest, this repo's six projects, and the real
testing approach (in-memory SQLite via TestDatabase, model calls faked at the
IAgentModelClient seam). Added sections the standards did not cover: the
three-front-ends-one-API rule, PATCH semantics, and a note that build-and-tests
green is not the same as working, with the commands to actually run each piece.

Code brought into compliance:

- Removed sealed from five types (the standard says no sealed)
- NovelAgentToolset.ExecuteAsync returned a named tuple; it now returns an
  AgentToolResult record (the standard says no tuples for return types)
- Added LangVersion latest to all six csproj files

None of the style rules produce build warnings — the IDE analyzers behind them
are off unless EnforceCodeStyleInBuild is set, and verified they stay silent
with it on too. 44 tests still pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
2026-08-06 12:11:20 -07:00
James WamplerandClaude Opus 5 7678cc7275 Replace the outline tree with chapter beat tables and tags
The self-nesting outline tree was more structure than chapter outlining needs.
A chapter outline is now a paragraph plus a flat, ordered table of beats, and
tags do the cross-referencing that nesting was doing badly.

A beat is one row: a three-to-five word title, an optional character, what
happened, and what's next. Ordering is a SortOrder column within the chapter —
no parent pointers, no cycle guards, no recursive tree building. Reordering is
one call taking beat ids in the order wanted; ids left out keep their relative
position at the end rather than jumping to the front.

Beats plan, scenes carry prose. The two layers stay separate and a beat's
SceneId is the optional link between them, nullable in both directions —
deleting a scene ungroups its beats rather than deleting the plan, since that
is a decision about prose and not about the outline.

Tags are project-scoped, unique by name case-insensitively, and attach to
characters, chapters and beats through three join tables so cascade deletes are
the database's job rather than ours. Applying an unknown tag by name creates it,
which keeps tagging a single action; GET /api/tags/{id}/references returns
everything carrying a tag across all three kinds at once.

Removed: OutlineNode, OutlineService, its endpoints, agent and MCP tools, and
the Outline tab. Added: Beat and Tag with their services, endpoints, 5 agent
tools and 10 MCP tools, a beat table on the chapter page, a tag editor used in
three places, and a Tags tab for cross-referencing.

Migration drops OutlineNodes — the scaffolder's data-loss warning is the
intended removal, not an accident.

44 tests, up from 31.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
2026-08-06 12:11:20 -07:00
James WamplerandClaude Opus 5 0d7b7a6f30 Add novel-writing app: .NET 10 API, React front end, agent and MCP server
Builds out the vertical slice for planning and writing a novel. Three front
ends — the React UI, an embedded Claude agent, and an MCP stdio server — all go
through one REST API, so an edit made from Claude Code and one made in the
browser are the same edit.

Layout:
  Domain          entities and enums, no dependencies
  Application     services, DTOs, the agent tool-use loop and its 15 tools
  Infrastructure  EF Core 10 + SQLite, Anthropic SDK client
  Api             ASP.NET Core 10 minimal APIs, OpenAPI, ProblemDetails
  Mcp             MCP stdio server, 21 tools over the same REST API
  Web             React 19 + Vite + TanStack Query + Tailwind v4

Data model is Project > Characters / OutlineNodes / Chapters > Scenes, plus
agent conversations. The outline is a self-nesting tree so acts, sequences and
beats can be arranged however the book wants; scenes carry goal/conflict/outcome
because that is what the agent drafts prose from.

Notes on a few choices:

- Conversation history replays to the model as text only. The agent re-reads
  current state through its tools rather than trusting a record of edits that
  may since have changed in the UI.
- The user's turn is persisted before the tool loop runs, so a question is
  recorded even when the model call fails. Turn order uses an explicit sequence
  column; timestamps tie when a turn completes inside one tick.
- Tool failures return is_error results rather than throwing, so the model can
  read the message and correct itself. MCP tools do the same via CallToolResult,
  which keeps the API's own message instead of a generic SDK error.
- The Anthropic client is constructed lazily. It is injected into the agent
  service, which also serves read-only endpoints, and those should keep working
  on an install with no key. Sending without one returns 503, not 400.
- DateTimeOffset is stored as UTC ticks. SQLite refuses to ORDER BY the default
  text form, which every "recently updated first" listing depends on.

Tests run against real in-memory SQLite rather than the EF in-memory provider so
they exercise the cascade deletes and query translation that actually ship.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
2026-08-06 12:11:20 -07:00