Commit graph

8202 commits

Author SHA1 Message Date
Anish Sarkar
03e9c27c9b test(model_connections): cover write-once seeding of the context limit 2026-08-03 14:42:28 +05:30
Anish Sarkar
b063230958 fix(model_connection_service): snapshot the lm studio native metadata payload 2026-08-03 14:42:25 +05:30
Anish Sarkar
b0ff176ff5 fix(model_connections): reject non-positive max_input_tokens on model update 2026-08-03 14:42:21 +05:30
Anish Sarkar
d9eb93deab fix(model_connections_routes): keep a stored context limit across rediscovery 2026-08-03 14:42:17 +05:30
Anish Sarkar
9bcbabcfdb test(llm_bundle): cover context resolution, num_ctx injection, and admission paths 2026-08-03 14:42:11 +05:30
Anish Sarkar
d2ef8513a6 feat(llm_bundle): attach the resolved context profile and pass num_ctx for ollama 2026-08-03 14:41:54 +05:30
Anish Sarkar
f48b615f39 feat(llm_config): resolve and enforce the model context budget on every generation path 2026-08-03 14:41:45 +05:30
Anish Sarkar
d8b309b41a refactor(llm_router_service): delegate trimming and message conversion to context_admission 2026-08-03 14:41:42 +05:30
Anish Sarkar
9f4ee014b5 test(context_admission): cover protected admission, router trimming, and tool budget 2026-08-03 14:41:38 +05:30
Anish Sarkar
8f9bde215e feat(context_admission): add shared message conversion and context trimming 2026-08-03 14:41:25 +05:30
Anish Sarkar
6def349137 fix(model_connections_routes): improve error message for model discovery failure by simplifying detail output 2026-07-31 21:45:16 +05:30
Anish Sarkar
c4f4a6c70d refactor(test_lm_studio_discovery): rename test for clarity and update assertions to reflect error handling for unavailable native APIs 2026-07-31 21:43:12 +05:30
Anish Sarkar
0e6f3264cb fix(model_connection_service): enhance error handling for LM Studio model discovery by raising ModelDiscoveryError with specific upgrade instructions 2026-07-31 21:39:58 +05:30
Anish Sarkar
e7523132c0 refactor(test_lm_studio_discovery): restructure model response format in tests to align with updated API structure 2026-07-31 20:11:00 +05:30
Anish Sarkar
0c06679c17 fix(model_connection_service): update LM Studio native v1 model response handling to check for 'models' key in payload 2026-07-31 15:37:30 +05:30
Thierry CH.
4e9225b1cd
Merge pull request #1650 from CREDO23/revert-kb-git-mvp
Revert "Merge pull request #1649 from CREDO23/kb_git_mvp"
2026-07-31 01:55:03 +02:00
CREDO23
a8292f5dcc Revert "Merge pull request #1649 from CREDO23/kb_git_mvp"
This reverts commit 366f9c18a, keeping the git-native knowledge base in
dev until the issues queued for the next release are fixed.

Nothing shipped by that merge was reachable in production: git-native
behaviour needs both the KNOWLEDGE_STORE_ENABLED env (default FALSE) and
a per-workspace column (default false), so reverting changes no runtime
behaviour.

Migrations 175 and 176 stay applied in production; alembic_version is
moved back to 174 by hand so this tree's upgrade is a no-op. Both
revisions only add columns, and they use ADD COLUMN IF NOT EXISTS, so
re-landing this work re-applies them harmlessly.
2026-07-31 01:46:56 +02:00
Thierry CH.
366f9c18af
Merge pull request #1649 from CREDO23/kb_git_mvp
feat(knowledge_store): make git the source of truth for workspace documents
2026-07-30 22:40:26 +02:00
CREDO23
bdabc2e7a3 docs(plans): record the measured test baseline in the runbook
Names the 6 pre-existing failures and the 13 fixture-absent errors, so a
future run can tell inherited red from its own.
2026-07-30 22:31:10 +02:00
CREDO23
586bca8326 style: apply ruff format to four knowledge store files
Line wrapping the formatter wants; code-quality gates on
ruff format --check.
2026-07-30 22:25:36 +02:00
CREDO23
11ae9039fd Merge remote-tracking branch 'upstream/main' into kb_git_mvp 2026-07-30 22:23:05 +02:00
CREDO23
10b8d4c9b4 docs(plans): add the production seed and flip runbook
Phase 5 says what the migration is; doing it on production needs an
ordered list of commands and checks. Covers the merge target and why it
is main rather than a dev promotion, the deploy checks that can corrupt
data (split object-store volume) or fail quietly (worker queues, beat,
schema), the dry run, the seed, parity verification, and the batched
flip with its rollback.

Also ignore the fleet runner's report file: it lands in the backend cwd
and carries workspace ids and document paths.
2026-07-30 22:22:58 +02:00
CREDO23
6ebd7ca7e5 Let a document's row follow its file when the file moves
Git reported a move as an unrelated removal and an addition, so the index
deleted the row and inserted a fresh one at the new path. The id is what
the rest of the schema holds: document_versions cascades from it, so the
document's entire version history went with it; an upload's stored original
cascades too, orphaning the blob; document_revisions is nulled, detaching
the audit trail; and citations already written into past answers name the
document and chunk ids, so every one of them dangles. move_file is an
ordinary agent operation and nothing warned — the sweep looked clean,
because git and postgres agreed on content.

Dulwich detects renames and tree_changes already accepts the detector, so
the engine now reports a move as one renamed change carrying both paths.
Convergence hands the row's marker over to the new path and the upsert
updates it in place. The fallback identity travels with the marker, or the
next file written at the old path would resolve to the row that left it —
except for an upload, which identifies by filename and would be duplicated
on re-upload.

The change window is one diff of the two snapshots now, rather than a fold
of every revision between them. A queued run can be several commits behind,
and folding lost the rename as soon as anything edited the file afterwards.
It also drops a path that came and went inside the window, which the fold
reported as a deletion.

A move that also rewrites the file has nothing left to match and still
arrives as a removal plus an addition; the marker guard in _delete is what
keeps that from deleting the row the same run just wrote.
2026-07-30 21:22:11 +02:00
CREDO23
6739d2774c Leave the working copy alone while a turn waits for approval
The end-of-stream safety net ran on every stream, and a turn paused at an
approval gate ends its stream like any other. The helper it calls both
commits and discards, so the turn's work so far was cut into a revision of
its own and anything not yet committable was dropped — including a folder
the agent had just made, which git cannot restore, having no empty
directories. The write the approval was granted for then failed for want of
its parent, and the next approval repeated the whole cycle.
2026-07-30 20:52:10 +02:00
CREDO23
f66448b946 Keep a rename from deleting the row its own upsert just wrote
Convergence applies upserts before removals, and git reports a rename as
both. The upsert claims the row by its path marker; the removal then
resolves the old path through the unique-hash fallback — stale, since a
retitle moves the marker and leaves the hash behind — and lands on that same
row. The document vanished from the UI and from search while its file stayed
in the tree, healed only by the next full rebuild.
2026-07-30 20:19:36 +02:00
CREDO23
9d775bb4fb Move a document's file only when its title was chosen, not inferred
A note's title is re-read from its first heading on every save, so an
ordinary editor save reached the recorder looking like a retitle and renamed
whatever the agent had named. On the canary that moved a note out from under
the path the agent was holding, and its next turn re-created the file
instead of editing it.
2026-07-30 20:19:30 +02:00
CREDO23
1fe8e149b0 Open a redis client per lock, so a second celery task can still index
Celery runs every task on a fresh event loop, and the cached client stayed
bound to the first one. It failed inside acquire — after redis had set the
key, before the reply was read — leaving the lock held by nobody for its
full TTL, which wedged indexing for the workspace.
2026-07-30 20:19:25 +02:00
Anish Sarkar
704f361f8e feat(model_utils): introduce capability fields mapping for improved model capability checks 2026-07-30 23:31:16 +05:30
Anish Sarkar
32c48b8e29 test(lm_studio_discovery): add unit tests for LM Studio model discovery and error handling 2026-07-30 23:31:07 +05:30
Anish Sarkar
2216be5e22 feat(provider_registry): add 'lm_studio_models' to DiscoveryKind and update registry entry for LM Studio 2026-07-30 23:30:58 +05:30
Anish Sarkar
b66e90ffb1 feat(model_connection): implement LM Studio model discovery and enhance error handling for connection verification 2026-07-30 23:30:47 +05:30
CREDO23
d263fc7de7 Read a document's path off the row, rather than guess it from the title
Two writers name files and they disagree. The seeder and the revision recorder
derive a name from the title, always appending .xml; the agent's write_file
commits whatever name the model chose, usually .md. So deriving is a guess
about anything the agent authored, and the canary's parity check read that
guess as drift: missing=1 extra=1 for a single file that was never wrong.

Worse than the false alarm, a real seed acts on it — writing the derived name
and deleting the agent's file as an orphan.

virtual_path_of prefers the path recorded on the row and falls back to
derivation, which is the name the seeder gave every row that has no marker.
The seeder now stamps what it actually wrote, so a retitle can tell which file
to drop, and the readers that hand paths to the agent report where content
lives rather than where a title implies it should.

Retiring .xml from storage is the real repair; this makes the two views agree
without renaming a single existing file.
2026-07-30 19:28:47 +02:00
CREDO23
d8468bf8bc Bound the commit subject's generation, so a stalled model can't hold a turn
The canary's turn hung at the very end with no error: the model accepted the
request for the revision's subject and then never answered. Everything that
closes a turn waits behind that call — the commit, the working copy's discard,
the turn's outcome — and a stall raises nothing, so the existing except never
ran and the stream just sat there.

A deadline is what the fallback needed to be reachable, since the subject is a
nicety and losing it costs a nicer name, not a write.
2026-07-30 19:28:36 +02:00
CREDO23
8c27559f9a Cut the narration out of the comments the last two commits added
The staging guards now read like their four siblings — one line naming the
reason — and the copy-id docstring keeps the invariant without retelling the
bug. 22 lines of prose the code already said.
2026-07-30 18:10:59 +02:00
CREDO23
521220c533 Record the two defects the canary's first turn exposed
Both lived in the write path the plan already claimed was shipped, so the plan
should say what the live test found and why the suite was blind to it.
2026-07-30 18:03:19 +02:00
CREDO23
5a392761ab Stop write_file and edit_file staging what the git backend already wrote
Both set dirty_paths under is_cloud with no backend check, so kb_persistence
recorded the same write into Postgres as a legacy document git never hears
about. That masked the dropped delegated writes — the note showed up in the UI,
nothing errored — and would have become a double write once they landed.

The four pure-staging tools already branch on the backend; these two stage as a
side effect of a successful write, which is how they were missed. With all six
guarded, none of the five keys the legacy commit triggers on is reachable under
the git backend, so it is a true no-op on a flipped workspace. files stays: it
is the in-turn read cache, not a trigger.
2026-07-30 18:03:13 +02:00
CREDO23
f88b80a1c0 Share one working copy per turn, so delegated writes aren't dropped
Subagents run under a namespaced {parent}::task:{tool_call_id} thread id, so
they opened a copy of their own while the end-of-turn commit — which only knows
the parent thread — diffed a copy that never existed. FileNotFoundError, a
silent return, and the delegated write gone, with the copy leaked because the
discard was never reached. Delegation is the normal path for agent writes.

The copy belongs to the turn, not to the actor: resolving the root segment puts
every actor in one copy, which is also what keeps one turn to one revision.

The suite missed this because every test builds the copy id by hand, so none
exercised the two sides deriving it. The new test writes through the backend
with a ::task: runtime and commits with the parent id.
2026-07-30 18:03:03 +02:00
CREDO23
a336d37320 Document the knowledge-store env, which nothing advertised
Neither .env.example nor any doc mentioned KNOWLEDGE_STORE_ENABLED, so
the flag was discoverable only by reading app/config. Names both keys,
and spells out the part that is easy to get wrong: this is the master
switch alone, a workspace also needs its own column set by the migration
runner, and the root has to be a shared volume because every web and
worker process reads the same history.
2026-07-30 16:49:57 +02:00
CREDO23
da0e5c6102 Stamp the index revision when flipping, so the seed's saving survives
The seeder deliberately never reindexes: it copies bytes out of Postgres,
so the chunk index already matches what it wrote, and passing parity *is*
that assertion. But nothing wrote last_indexed_revision — the only writer
in the tree is converge.py, inside a convergence run — so a flipped
workspace was left with a NULL stamp.

The drift sweep reads NULL as never-indexed and enqueues a whole-tree
converge, which re-embeds everything an hour after the flip. That is
exactly the migration cost the adopt-don't-rebuild seed exists to avoid,
paid silently and unprompted.

Flip now stamps the store's head, read from head rather than the report's
seeded_revision, which is None on an idempotent re-seed. Unflip clears
it, so a workspace flipped back converges fully instead of trusting a
stamp from before the legacy pipeline owned its chunks.

Found by canary-flipping workspace 1 before touching the fleet. Verified
by mutation: dropping the stamp fails the two flip tests, and stamping
unconditionally fails the unflip test.
2026-07-30 16:37:41 +02:00
CREDO23
fc9c7b4e16 Assert restore survives the global flag for an unflipped workspace
The restore guard had only its positive case: refused when the workspace
is git-backed. Nothing held it to being per-workspace, so rewriting it
as load_knowledge_store_settings().enabled would take restore away from
every workspace on the fleet the moment the kill switch goes on, and the
suite would not notice.

This is the asymmetric case test_writer_guards.py already keeps for the
editor reindex guard; the restore guard now has it too. Verified by
mutation: swapping in the global-flag form fails this test alone.
2026-07-30 15:40:59 +02:00
CREDO23
fa2d85eb8e Pin the receipt a turn emits for every kind of change, not just adds
Each existing test wrote the same brand-new documents/note.md, so
_OPERATION_BY_KIND was only ever exercised for "added" and tx.remove
never ran at all. Mapping "removed" to write_file would have left the
suite green — and receipts are what the orchestrator treats as ground
truth for what the agent did.

Adds an edit, a delete, a move (which the store decomposes into the two
changes, so it covers both kinds at once), a mixed turn asserting one
receipt per change, and a contended commit carrying a removal so the
failed-receipt branch stops being add-only.

Verified by mutation: collapsing the kind map fails the four change-kind
tests while all eight originals pass, and hardcoding the failed-receipt
operation fails only the new contention case.
2026-07-30 15:40:53 +02:00
CREDO23
23d0b1da91 Cover the editor and sync recorders, not just the batch primitive
Every test in this file called record_markdown_files directly, so the
retitle test handed `removes` in ready-made and proved only that the
primitive honours it. The callers that have to *derive* the removal from
the row's path marker were never invoked: record_saved_document and
record_prepared_documents were both at zero coverage, taking the module
to 46%.

Drives both through the real git engine: retitle leaves one path and one
extra revision, no marker is left when nothing was recorded, a marker
outside /documents is swallowed rather than fatal, a document with no
source_markdown stays out of the batch, and neither caller lets a store
failure reach the save that already committed.

Module goes to 100% statements and branches. Verified by mutation: never
deriving the stale path, writing the marker without a revision, and
dropping the markdown-less skip each fail exactly one new test.
2026-07-30 15:40:42 +02:00
CREDO23
a2025f8829 fix(agents): stop read_file promising a citation envelope two modes never render
select_description ignored its mode argument, so every mode was told reads come
back as <document view="full"> with [n]-labelled passages, and to cite "the same
[n] you would use from search_knowledge_base". Only KBPostgresBackend renders
that. On a git-native workspace the model is promised labels it never sees while
holding search ordinals in context, so the likely outcome is not a citation that
drops but one that credits the wrong source — the defect class C2 exists to kill,
and the only thing about C2 that blocks flipping a workspace.

Split by read format rather than cloud-vs-desktop, which also fixes desktop
local: it has carried the same wrong description since before the flag existed.
Raw-mode text now forbids reusing a search ordinal outright.

knowledge_store_enabled threads from the middleware stack through the subagent
dependencies to build_filesystem_mw; it already keys the compiled-graph cache,
so a flip rotates the description with the backend.

The envelope itself stays deferred to its own post-flip PR: search citations are
unaffected by the flip, and C2's remaining slices need spans that only exist
once the indexer has run.
2026-07-30 14:45:32 +02:00
CREDO23
f04cfb387e docs(celery): the drift schedule repairs now, not just alarms 2026-07-30 14:07:02 +02:00
CREDO23
baca014e41 feat(knowledge_store): repair Postgres-side drift instead of alarming on it
The hourly sweep compares a stored git revision against the store's HEAD, so
both sides of its predicate come from git and it cannot see drift that lives on
the Postgres side. The daily monitor does see it, but only logged — leaving the
fix to a hand-typed reindex call, which contradicts Phase 4's own "runbook steps
get forgotten; converging systems don't" for the identical class of fault.

A drift verdict now enqueues the whole-tree converge, capped at 10 per run:
fleet-wide drift is systemic, and fanning out rebuilds would compound it rather
than fix it. The error status stays alarm-only, since a store the check could
not read is not fixed by indexing it harder and its parity fields describe
nothing.

Ceiling marked in the code: drift index_tree cannot fix — an unmarked Postgres
row with no file in the tree, i.e. a writer bypassing git — costs one rebuild
per run until a human intervenes, with the alarm persisting throughout.
2026-07-30 12:56:31 +02:00
CREDO23
56fdda6871 refactor(knowledge_store): name the two index entry points for their scope
index_revision promised an argument it does not take — its docstring had
to open by disclaiming it — and reindex is the same verb the legacy
per-document editor path already uses, which is the writer this phase
guards against.

Fossil's rebuild was the obvious alternative and is wrong here: the run
upserts and prunes, and document ids survive it because they reach the
browser through the Zero publication. Nothing is wiped but chunks.

The two differ only in scope, so the names say scope: index_changes
folds in what moved since the stamp, index_tree reconciles against the
whole tree and is the only one that can notice a deletion it never saw.

Celery task names are untouched; they are the wire contract.

Also corrects the umbrella plan, which described this as "wipe +
rebuild" while 04-derived-index.md said document rows are never wiped.
2026-07-30 12:17:10 +02:00
CREDO23
58126c5d30 refactor(knowledge_store): group the derived index into its own package
The core package was clean apart from two files: everything under
knowledge_store imports only itself and app.config, while indexer.py
reached into app.db, the indexing pipeline and the agents middleware.
Per ADR 0002 that file is a driven consumer, not core, and nothing in
the layout said so.

Moves it to knowledge_store/index/ as converge.py + queue.py, and the
three celery entry points to celery_tasks/knowledge_store/. Task names,
queue routes and beat entries are unchanged; only module paths move.

index/__init__.py deliberately re-exports nothing. queue.py is a
writer's last step and has to stay cheap to import, so a convenience
re-export would put the ORM and the pipeline on every save's import
path. A test pins that, and fails if the re-export comes back.
2026-07-30 11:51:18 +02:00
CREDO23
49f7c89414 test(tasks): cover the drift monitor's verdict and per-workspace isolation
This check is what replaces reading migration reports by hand once
workspaces are flipped, so an "ok" it reports has to mean git and
Postgres actually agree. Runs against real repos and rows: a seeded
workspace reads ok, an unseeded document reads drift, an unflipped
workspace is not checked, and one failing workspace still leaves the
next one checked.

Mutation-verified: collapsing _status to a constant "ok" fails.
2026-07-30 11:21:40 +02:00
CREDO23
884b05a084 test(knowledge_store): pin enqueue_index's swallow and its happy path
enqueue_index deliberately swallows broker failures so a committed save
never fails on a queue problem. Untested, that swallow makes a typo
look exactly like a broker outage: nothing enqueues and nothing
complains. Covers the dispatch, the kill switch, the non-numeric id
drop, and the swallowed connection error.
2026-07-30 11:21:40 +02:00
CREDO23
28a5eaf60c test(tasks): cover the drift sweep's candidates and queue routing
The sweep is the only recovery for indexing lost to a broker or worker,
and it was the one piece of phase 4 with no test at all. Pins the
flipped-only filter, the skip when a stamp is level or a store is
missing, the enqueue cap, and the routing split where a never-indexed
workspace goes to the rebuild task rather than the fast queue.

Both of those last two were verified by mutation: dropping the flipped
filter and inverting the routing each fail a test.
2026-07-30 11:21:28 +02:00