Repository navigation
Releases: Merp4/dexicon
Release list
v0.6.10
⚠️ Upgrading
- One migration,
KeyReachIsExplicit, addstokens.AllCorpora. Whether a key reaches every
corpus is now a setting on the key. The migration sets it for every key that has no corpus rows
at that moment, which is what those keys reached, and leaves a key that has rows restricted to
them. A key that the removal of its only corpus had already emptied, in an earlier version, has
no rows either and becomes a key that reaches every corpus: the catalogue does not record that
it was restricted. Before upgrading, readGET /api/tokensand look for a key with an empty
corpusIdsthat should not reach every corpus; restrict it again with
PUT /api/tokens/{id}/corpora({"allCorpora": false, "corpusIds": [...]}) or revoke it.
Going back to 0.6.9 without restoring the backup taken before the upgrade makes a key
restricted to nothing reach every corpus again, and upgrading a second time does not repeat the
backfill (D-42). - Key requests that answered
200or500are refused.PUT /api/tokens/{id}/corporawith
{"corpusIds": []}is400(it was a200that lifted the restriction), and so isPOST /api/tokenswith an emptycorpusIdsand noallCorpora; sendallCorpora: trueor leave the
field out.PUTwith{}is400titledSay which corpora(it wasCorpus ids are required). A request that names more than 1,000 distinct corpora is400(a very long list was
a500).corpusIdsis empty both for a key that reaches every corpus and for one restricted
to nothing, so a client readsallCorpora. An Access page opened before the upgrade sends the
old request and gets400until it is reloaded. - Detaching the last attachment of an uploaded document deletes the document
(D-43): its
blob row, its extracted text and its bytes under/data/blobs. The same happens when a
replacement by name, the removal of an upload source or the removal of a corpus takes the last
reference. Uploading the same bytes again stores and extracts them again. The first start after
the upgrade also deletes every stored document that no corpus holds (what 0.6.9 and earlier left
after a detach), and then the files under the blob root that no document names: the bytes of an
upload whose extraction failed or timed out, which 0.6.9 listed as kept and listed nowhere, the
temporary file of an upload that a crash cut short, and bytes whose delete failed. Going back to 0.6.9
does not restore them. Count the documents nothing holds before upgrading:
SELECT COUNT(*) FROM blobs b WHERE NOT EXISTS (SELECT 1 FROM files f WHERE f.BlobSha256 = b.Sha256).
Restore the catalogue and/data/blobstogether: a catalogue restored alone, older than the
files beside it, makes the newer files look stray, and the sweep deletes them. - Attaching a stored document by its hash needs a corpus the caller reaches that holds it.
POST /api/corpora/{x}/documents/attachanswers404 No such documentotherwise, for a key and
for an administrator, since a document that no corpus holds no longer exists. A key could attach
any stored document by its hash before; it now sees only the documents of corpora it reaches. - A key that is revoked or has expired, and an administrator session that has signed out, stop
receiving progress events from the next heartbeat./healthzcounts the corpora a key reaches
and reports a running job only in one of them (it counted every corpus for any key).
Fixed
- A key restricted to a corpus reached every corpus once that corpus was removed. Removing a
corpus deleted the key's rows, and a key with no rows reached every corpus. That held for an
administrator's delete and for an approved removal, and a key holdingproposecould then ask
for the removal of any corpus it could name. It dates from the first scoped keys. A key restricted
to a removed corpus now reaches nothing. The Access page offers Every corpus and Only
these, and does not read nothing ticked as every corpus. - Removing a corpus while a key was being created for it, or while its reach was being
replaced, answered500on a foreign key. The removal and those two requests now wait for each
other. - Removing a corpus, a source, a chunk set or a document answered
500when the Qdrant
collection holding its vectors did not exist, as after a lost or replaced Qdrant volume or a
catalogue restored beside an empty Qdrant. A delete in a missing collection has nothing to do and
succeeds, and the log says the collection was not there. Any other failure of the vector store
still stops the removal. - A dialog whose title names a file with no space to break at no longer scrolls sideways at phone
width. - The message for an upload whose extraction failed on the server no longer says the bytes stay in
the blob store. They stay until the next start, which deletes them.
Changed
- The Documents page asks before a detach and says whether another corpus holds the document. The
Approvals dialog for a document or a corpus, the confirmation for deleting a corpus and for
removing the uploaded-documents source, and thedestroyscope description say that documents
no other corpus holds are deleted. - The 409 answer for a corpus name that is taken, or that is another corpus's id, no longer repeats
the stored name of the corpus that holds it. That a name is taken is still told, since names are
global. - The skill is unchanged (
dexicon-skill-version: 7). - Dependency updates: AngleSharp 1.8.4, radix-ui 1.7.0, lucide-react 1.55.0, and two groups of
build and test dependencies. - The PDF limit in
docs/10-security-secrets.mdno longer gives the per-platform depths measured
in probes, and advises indexing and uploading PDFs only from sources you trust.
Known limits
- An upload of bytes at the moment the last holder of the same bytes detaches them can fail once,
with a404from attach, or with an extraction failure when the bytes were deleted while it read
them; uploading again works. A blob row stored between the release's check and its byte delete
keeps its text and loses its bytes, so it searches as any other document and is not extracted
again after an extractor change. - The sweep of
/data/blobsassumes one process owns the data folder. A catalogue with no document
at all leaves hash-named files in place and logs a warning, so a catalogue emptied by detaching
every document keeps any leaked bytes until a document is stored. - The removal of an upload source or a corpus releases its documents one at a time, under the
attachment lock and, for a corpus, the naming lock. The time for a corpus of thousands of
uploaded documents was not measured. - An upload of bytes that a key already has still answers
deduplicated: trueand the title of a
document that another corpus holds, so it tells the key that those bytes are stored there. It
needs the bytes.
docker pull ghcr.io/merp4/dexicon:0.6.10
Digest sha256:46fb8b753aa2ca6d0fdc15f3f813a10a79478a3a38d2d6826156ef8b52bc81b2. An SBOM and build provenance are attached to the
image in the registry.
v0.6.9
⚠️ Upgrading
- No migration, no new setting and no change to
docker-compose.yml. Data written by 0.6.9 reads in
0.6.8, except that a history source that stored/docsfails again there. - Uploads are bounded in more ways, and some answers change.
- A request is read for at most ten file parts and 100 multipart sections, form fields included.
Past either limit the application reads no more and answers202with a request-levelfailed
entry (fileisnull); parts it did not read are in neitherstorednorfailed, so a script
readsfailed. When none of the first ten files was stored the answer is400"No files could be
stored", or400"No files in the request" (its detail repeats the text only when the section
limit was hit), or503when at least one failure was server-side. The transfer is not stopped,
so no bandwidth is saved. - A file name is refused on upload (listed under
failed) and on attach (400"Invalid file
name") when it holds a control character (line breaks, U+2028 and U+2029 included), a
bidirectional override or isolate (U+202A to U+202E, U+2066 to U+2069), or is longer than 260
UTF-16 units. U+200E, U+200F and U+061C pass. Attaching with a blankfileNameanswers400;
leaving it out keeps the name the document was uploaded under, and400follows if that name
breaks the rule, so a script that attaches by hash alone gets400for such an older document. DEXICON__INDEXING__EXTRACTIONTIMEOUTSECONDS(300; 0 disables) applies to uploads as well. An
upload that exceeds it, or fails for a reason that is not a verdict on the file (an I/O error,
a refused permission, a shortage of memory, aTimeoutException, a library cancellation), is
listed underfailedand no document record is created; the bytes stay in/data/blobs, up to
DEXICON__UPLOAD__MAXFILEBYTESeach, listed nowhere. Sending the file again extracts it again
for the full budget, with noRetry-After. A concurrent upload of the same bytes is reported as
stored.- A
blob_textsrow written by 0.6.8 or earlier for one of those failures keeps its message as
the document's empty reason. It is at the current extractor version, so no pass extracts it
again, and uploading the same bytes returns it as already stored. Nothing in the product clears
such a row; a later extractor version extracts it again.GET /api/documentslists the reason
of each document that has no text. - A DOCX, PPTX or EPUB package with an XML part nested deeper than 512 levels, or XML that
inflates to more than 256 MiB in total, is an unreadable document. An HTML file or EPUB chapter
with more than 1,000,000<characters is too. The same limits apply to workspace files: a new
or changed file over them is an unreadable document, and a file already read is not read again.
HTML has no nesting limit: its parse is cancelled when the extraction clock runs out, so a
document too deep to parse within the budget is a timeout, and with a budget of 0 the HTML parse
is unbounded in time.
- A request is read for at most ten file parts and 100 multipart sections, form fields included.
- Creating a corpus checks its chunk settings. A client that posted a size outside 64 to 8,192,
an overlap that is negative or not below the size, or an unknown boundary mode to
POST /api/corporaand relied on201gets400, andcustomcannot be set at creation. A
server that setsDEXICON__INDEXING__CHUNKSIZE,CHUNKOVERLAPorBOUNDARYMODEoutside those
rules logs an error at startup and answers503to a creation that does not send its own value
for the setting that is out of range. A size under 64 or over 8,192, an overlap equal to the size
and a negative overlap used to create corpora that indexed; an unknown mode used to create a
corpus whose code and text files all failed.docker-compose.ymldoes not forwardBOUNDARYMODE;
set it indocker-compose.override.yml. A chunk set added from a template inherits the template's
size and overlap as stored, even when the current rules would refuse them. - Glob lists are capped, and git pathspecs are checked at save. A list holds at most 200
elements and an element at most 500 characters. A history source's include list, and a corpus
default include list (which history sources inherit), are refused when an element makesgit log
fail on every pass: an empty element, an element holding a null character, malformed pathspec
magic, a path that climbs out of the repository, a rooted path, or an element starting with/:;
the rules are those of git 2.54.0 on Linux. The web UI forms andconfigure_corpussend stored
lists back, so a stored list that fails a rule blocks any save that sends it until it is edited or
reset (resetinconfigure_corpus). That includes a default over the caps and one stored with
an empty element,//x,/:x, a climbing path, a rooted path or malformed magic, all of which a
file source read without error. No stored row changes. A history source that stored/docsfails
again on an older version. - Ignore files are read with limits, and a breach fails the source. A walk reads at most 5,000
rules and 60,000 pattern parts across all ignore files, a file of 1 MiB, and 16 MiB of ignore
files. A stored list is held to 1,000 rules and 20,000 parts. Passing any of these fails that
source's walk, for.gitignoreand.git/info/excludeas well as.dexiconignore: the source's
files are not indexed or removed, the corpus reads unavailable and the reason is in
index_status, naming the files that used most of the budget and, when a.gitignoreused some
of it, offering to turn offuseGitignorefor the source (in the web UI and over REST; MCP keeps
it on). A line or list entry over 500 characters fails the walk in a.dexiconignoreand in a
list, and is skipped with a warning in a.gitignoreor.git/info/exclude. A stored list with
an entry over 500 characters, a POSIX class, more than 1,000 rules or a weight over 20,000 makes
its source unavailable on the first pass after the upgrade, until the list is edited. A
.dexiconignorethat has an unusable line, a carriage return inside a line, a link, a pipe, an
unreadable file, a NUL, bytes invalid in its encoding or a twin differing only in case fails the
walk. Before, a link was skipped, a NUL or an invalid byte was read as a character that matched
nothing, and a lone carriage return ended a line, so a file with old Mac line endings applied and
now fails the walk; lines end at a line feed, as in git. A.gitignoreor.git/info/exclude
with CR-only line endings is read as one line, matches nothing and raises no warning; one that
holds a NUL is skipped whole with a warning, where only the NUL line was inert before. Files they
excluded are indexed on the first pass. Ignore files are found by name without regard to case on
every platform: on Linux a.GitIgnoreor.DexiconIgnorethat was ignored before is now read.
Byte-order-marked UTF-16 and UTF-32 files are still decoded. - Bracket classes and some patterns are read as git reads them. A class written with a leading
!, such as[!a]*.md, matches any character except those listed. No class matches/; a
positive class that spanned it, such as[+-9], matched it before. A backslash inside a class
escapes the next character ([\d]is the letterd, not a digit), and[a-z-[b]]is the
membersa-z,-,[andbfollowed by a literal], where it was class subtraction. A
leading], a[and a trailing-are members.
POSIX classes, any other name between[:and:], empty classes and reversed ranges ([z-a])
are refused as unusable lines. Three or more stars forming a whole segment are one**. Lines
are split at line feeds. On the first pass after the
upgrade, a source whose patterns change meaning indexes the files it now selects and drops the
ones it no longer does. Differences from git remain and are listed indocs/04-ingestion.md. - Removal, attach and promote answer differently. The second of two simultaneous removals of one
source, chunk set or corpus answers404("removed while this request waited"). A laterDELETE
of a source or a chunk set answers404, and a laterDELETEof a corpus answers400from the
scope resolver. An upload or attach that waited behind the removal of its corpus answers400as
an unknown corpus. A chunk set that became the default or the only set while its vectors were
being deleted is kept and the removal answers409; a refresh of the set is queued, and a
proposal to remove it stays waiting. - The embedding model probe needs the admin session.
POST /api/embedding-models/probeanswers
403to a key, whatever its scopes; 0.6.8 accepted a key withsearch. - Log lines and tool replies change. Characters the console template leaves alone are replaced
with U+FFFD in logged values: DEL, C1 controls, line and paragraph separators, bidirectional and
other format characters, and a zero-width joiner or non-joiner, so a family emoji or a Persian
word written with a ZWNJ shows the marker. A line break in a logged value shows as\ninside
quotes. An exception is written a line at a time: each line is cut at 4,000 characters, the rest
after the line that crosses 64,000 (about 64,000 in all) and a string property at 8,000, and a
line the runtime did not write is indented by four spaces. MCP tool errors show a caller's value
on one line cut at 200 characters, stored text on one line cut at 4,000, only three unknown corpus
names and a corpus list cut at a whole name within 1,500 characters; thedetailof the REST
400for an unknown corpus is cut the same way. An agent no longer sees a provider's message or a
path the workspace resolver refuses; the log has them. - The skill changes (
dexicon-skill-version: 7). It tells the agent th...
v0.6.8
⚠️ Upgrading
- One migration,
FoldTaggedModelNames, applied at startup. Data only: it renames a saved framing
or a measurement whose model is spelled with a:latesttag (in any letter case) or with capital
letters to the lower-case name without the tag, which is the name the indexer reads. Where several
spellings of one model have a row it keeps the newest (UpdatedUtcfor a framing,MeasuredUtc
for a measurement) and drops the others. A chunk set whose framing was saved from the tagged
Models row picks it up on its next pass and re-embeds, which is what saving the framing already
said it would do. A measurement stored under the tagged name now applies to a set that records
the untagged one: the set's chunk budget follows the measured characters per token instead of the
default of 4, so that set re-chunks on its next pass. A 16-file set onnomic-embed-text, which
measures 2.82, went from 217 to 303 chunks. The migration cannot be undone. - A document upload is bounded as a request: ten files at
DEXICON__UPLOAD__MAXFILEBYTESplus
1 MiB of multipart framing, which is 2,098,200,576 bytes at the default. A body that declares a
larger length, or that crosses the bound before any file is stored, answers413"Upload too
large" with a message that names the bound. When files were stored before the bound was
crossed they stay stored and the answer is202, with the overrun listed underfailedwith a
nullfile, so a script checksfailedand does not treat every202as a full success.
failed[].fileisnullfor any failure of the request as a whole and a file's name otherwise.
Only the per-file cap applied before. A script that posts a batch beyond the bound needs to
split it. - Three requests are refused that were accepted or answered
500.POST /api/corpora/{x}/sources
withoutworkspacePathis400: it was read as the workspace root, and an empty string still is
the root.PUT /api/tokens/{id}/corporawithoutcorpusIdsis400, and an empty list still
lifts the restriction.POST /api/tokenswith anexpiresInDaysbelow 0 or above 36,500 is
400; a negative number made a key that never expired. - The skill changes (
dexicon-skill-version: 6). Runningscripts/install-mcp.ps1again
upgrades installed copies. The hooks are unchanged.
Changed
- An upload is streamed into the blob store. The endpoint read the whole multipart form into
/tmp, a tmpfs in the compose file, before the per-file cap applied: a 400 MB file, sent with
DEXICON__UPLOAD__MAXFILEBYTESraised above its default of 209,715,200 bytes, held 400 MiB for
about 8 seconds. Each file now goes from the connection into the store under the cap, and the peak in
/tmpmeasured in the container was 0 for that file, a 50 MB file and a mixed batch. A file
over the cap or an empty one is listed in the response'sfailedarray and the files beside
it are stored. - The OpenAPI documents declare the status each operation returns. Twelve operations answered
201, 202 or 204 while declaring 200, so a generated client typed the wrong response. The
committed documents and the typed web client are regenerated. - The documentation was checked against the code and corrected. Statements that no longer
matched the code were found in the setup, install and upgrade procedures (README.md,
docs/09-deployment.md), the MCP, auth and integration references, the data model and the UI
guide, and were corrected.docs/09-deployment.mdhas an Upgrading section and a configuration
table keyed by the.envnames. The README quickstart was followed step by step on a fresh
clone, and the steps that failed were corrected. - The first-start log and the sign-in screen say how to read the admin password. It is the
quoted value on the third line afteradmin password, so the commands read
grep -A 2 "admin password". The banner no longer says the password can be changed in the UI;
a value set inDEXICON_ADMIN_PASSWORDis applied on every start and nothing else changes it.
The error logged when an embedding provider cannot embed names the provider and model it tried. scripts/backup.shstops Qdrant as well as Dexicon during the backup, restarts what it
stopped even when the archive fails, exits nonzero if that restart fails, and takes the Compose
project name from.env. It had archived an empty volume under a project name set there.- The Documents screen names each file the server refused. A drop of several files showed
only "N files stored", so a file over the size cap or an empty one vanished without a reason.
Each is listed with its reason under the count, and a failure of the whole request is listed
without a file name. - A test rejects the request's token after a catalogue write.
RequestTokenAfterCommitTests
compilessrc/and fails on a use of the request'sCancellationTokenthat a catalogue write can
reach, whether named, copied, linked, captured by a lambda or local function, or read from
HttpContext.RequestAborted, so a job, audit line or reply that follows a save on that token
is found where it is written. CatchingOperationCanceledExceptionaround the use does not clear
it. A function or type exempted by a[SuppressMessage]with a non-blank justification is skipped,
and aMessageIdlimits it to findings with that target. The test's summary lists what it does
not see, among them a token held in a field and acatchreached after a write, and the test
fails whensrc/gains a project it does not read or code under#if(CONTRIBUTING.md). - Stacked doc comments fail CI.
scripts/check-doc-comments.py, a step of the required
Build and testjob, fails on a/// <summary>line whose previous non-blank line is also
///. Inserting a member between another member's doc block and its signature leaves that
shape, and a documentation build does not report it. The script reads every tracked*.cs
file and fails when it reads none. Its one hit, a stale summary stacked on
CodeChunker.IsFenceDelimiter's, is removed.
Fixed
- A framing saved from the tagged Models row never reached a set recorded without the tag.
Ollama listsembeddinggemma:latestand a set recordsembeddinggemma; a saved framing and a
measurement were stored under the name they were sent with, so the indexer read a different
row and saving answeredreindexing: []. Both are now stored and read under the lower-case
name without:latest, saving rebuilds every set on the model whichever spelling it records,
and the rebuilds are not cancelled once the framing is committed. The default for new
corpora badge also requires the default provider. See Upgrading for the migration. - Two sources holding the same file path were read as one text.
GET /api/corpora/{x}/file
and thedexicon://file resource stitched the chunks of both into one text with no warning.
They read the source with the most chunks and say how many sources hold the path, as
get_contextalready did, counting a source whose copy is empty. - A body that ends before its closing boundary answered 500. Files completed before the cut
stay stored and are now indexed, the cause is listed underfailed, and with nothing stored
the answer is400"Malformed multipart upload". - A client that disconnected after documents were attached left them waiting for the next
refresh. The job that indexes an upload, an attach to another corpus, a new corpus created
with a folder, a new chunk set, or a chunk-set change that re-chunks was queued on the request's
token, so a cancel between the save and the job left the saved change with nothing queued. A new
chunk set stayed marked as degraded, and a change was not re-applied by sending it again. The job
is no longer cancellable once the change is saved, and an upload cut off partway keeps and
indexes the files completed before it. The file being attached at the moment of the cut is
discarded, not saved without being reported. - A key issued for chosen corpora could be saved reaching every corpus. The key and its
corpora were two saves: an unknown corpus id was refused with400after the key was saved,
and a cancel between the saves left it unmapped. A key with no mapping reaches every corpus,
and its secret had not been shown. The corpora are checked first and saved with the key. A key
an earlier version already left unmapped is not repaired:GET /api/tokenslists it with an
emptycorpusIds, nobody holds its secret, and it can be revoked. - Requests that answered
500, or were read as something else. Each was found by sending every
JSON-body operation an empty object andnull, by reading the attach path for a taken name, or by
an independent review of the request paths, and each was reproduced against a running build. A
refused request changes nothing.PUT /api/tokens/{id}/corporawithoutcorpusIdsthrew on the missing list. It answers400
and keeps the mapping.POST /api/tokenswithexpiresInDaysof 2147483647 threw inDateTime.AddDays, and a
negative value made a key that never expires. Values below 0 or above 36,500 days answer400
"Invalid expiry".POST /api/corpora/{x}/sourceswithoutworkspacePathadded a source over the workspace root,
so a misspelt field indexed everything mounted. It answers400.- Sending a document whose bytes are already attached to a corpus under the name of another
document in it renamed the first onto a path the source already held, and the save failed on the
unique key. An upload lists the file underfailedand stores the files beside it; an attach
answers409"Name already used". Bytes that are not attached to the corpus yet, sent under a
name it holds, still replace that document. - `POST /api/searc...
v0.6.7
⚠️ Upgrading
- One migration,
GrantDestroyToIngestKeys, applied at startup. Data only: it appendsdestroy
once to every key that holdsingestwhen it runs, so none of them stops being able to detach
a document. That includes the key adopted fromDEXICON__BOOTSTRAP__TOKENif its row already
exists. A token value adopted for the first time after the upgrade is a new key, holdssearch
andingestonly, and needsdestroyadded to detach. The migration cannot be undone, and a
0.6.6 started afterwards ignores the extra word. DELETE /api/corpora/{id}/documents/{fileId}needsdestroyand no longeringest. A key
issued from now on is given what is ticked, anddestroyis never ticked for a new key, so a
script that detaches documents with a new key needsdestroyadded, on the Access page or with
PUT /api/tokens/{id}/scopes. On a fresh install the key adopted from the environment holds
searchandingestonly.- Detaching applies to uploaded documents only. An id from a source that reads a folder or a
repository's commits answers 404; it used to remove that file's vectors and row while the
source still listed it.
Added
- A
destroyscope lets a key detach an uploaded document
(D-40). It is independent
ofingest, so a key can add documents without being able to remove them, and the other way
round. The Access page lists it, never ticks it for a new key, and warns that nothing asks
first. Sources, chunk sets and corpora stay out of every key's reach and are asked for with
propose.
Changed
- The 403 for a missing scope says who can grant it. Every scope check over HTTP told the
caller to sign in to the UI, which is true only ofadmin. The other scopes now say that
whoever runs Dexicon can grant them on the Access page, as the MCP tools do. dotnet formatand test names are checked in CI.dotnet format Dexicon.slnx --verify-no-changesis a step of the requiredBuild and testjob, and 13 files were
reformatted to pass it. An underscore in the name of a C# test method or class is a build error
(CA1707). Neither was enforced before (CONTRIBUTING.md).
Fixed
- The detach endpoint reached files a source had read. It took any file id in the corpus, so a
key holding the scope it needed (ingestuntil now) could remove a workspace or history file's
vectors and catalogue row by giving an id thatGET /api/corpora/{id}/fileslists to a key that
can search. Only an uploaded document is detached now, in the cleanup and in
DocumentService.DetachAsync, which the approved removal of a document uses too.
docker pull ghcr.io/merp4/dexicon:0.6.7
Digest sha256:5d566639a39c1aa87358debce2061b5ad0262a6343eb3566530e6d20204babf1. An SBOM and build provenance are attached to the
image in the registry.
v0.6.6
⚠️ Upgrading
- Two migrations, applied at startup, both additive.
FilesSettleBySizeAndModifiedTimeadds a
nullable column tofile_chunk_states.AddProposalscreates theproposalstable. Neither
changes an existing row. - The first pass over each chunk set after upgrading reads every workspace file once and writes
the settle keys (below), so a corpus with several chunk sets reads its files once per set.
Uploaded documents and git-history sources are unchanged. Nothing is re-chunked or re-embedded
for that. - A PDF whose cached text was cut short is extracted again and re-indexed on the next pass that
reads it. Nothing needs triggering by hand. On the library this was measured on, 164 files
were affected, which is about 60,000 more chunks to embed. - A key can hold a new scope,
propose, which no existing key has. The key adopted from
DEXICON__BOOTSTRAP__TOKENis not given it, and an administrator can add it to any key with
PUT /api/tokens/{id}/scopes.GET /api/proposalsand
POST /api/proposals/{id}/approveand/rejectare new, and take the administrator's session.
/healthzgainspendingProposalsin the administrator's reply. Additive. - The skill changes (
dexicon-skill-version: 5). Runningscripts/install-mcp.ps1again
upgrades installed copies. The hooks are unchanged.
Added
- A
proposescope lets an agent ask for a removal, and the administrator decides
(D-39). Two more MCP
tools are listed for a key holding it.propose_removalasks for a source, chunk set, uploaded
document or corpus to go, with a one-line reason, and removes nothing.removal_statuslists
the key's own requests and how each was decided, and needs no other scope and no corpus that
still exists.proposeis independent of every other scope: a key can hold it alone, and
holdingconfiguredoes not carry it. - An Approvals screen lists what agents have asked for. Each request says what would go and
how much (files or commits, chunks, chunk sets), who asked and why. Approving asks first, runs
the code the admin's delete runs, and records the approval in the same save. Rejecting leaves
everything as it is. The nav link carries how many are waiting.
Changed
- A settle key skips unchanged files on reindex
(D-38).
file_chunk_states.settled_forholds a hash of the file's size, modified time and chunking
settings, and a pass that finds the same key does not open the file. A document (PDF, DOCX,
PPTX, EPUB, HTML) is not opened at all; other files are still opened for the 8 KB check for a
NUL byte.FullandRebuildjobs ignore the key. - Corpus and chunk-set removal live in one service. The admin's
DELETEendpoints and
approving a request run the same code, so the two cannot drift. - Dependencies: lucide-react 1.52.0 and jsdom 30.1.2.
- Test methods are named as PascalCase sentences and test classes for the behaviour they cover
(CONTRIBUTING.md).
Fixed
- The cached text of a PDF could be cut at its first U+0000, and the document indexed from the
head of it. SQLite ends a text value there while the row recorded the full length. A cache row
is now a hit only when what it reads back is as long as what was written; otherwise the text is
extracted again, replaced and logged. An upload's repaired text also clears the chunk state of
every attachment of that blob, so the repaired text is chunked and embedded. - U+0000 is removed from every format's text where it is stored, not only a PDF's, with the
offsets of its units moved to match. A plain-text upload holding one was otherwise repaired,
and chunked and embedded again, on every pass.
docker pull ghcr.io/merp4/dexicon:0.6.6
Digest sha256:d9530deae1d7b587e1e38ff31b1ac796b6ebda386a598d9fba8d6ca25304e530. An SBOM and build provenance are attached to the
image in the registry.
v0.6.5
⚠️ Upgrading
- One migration,
IndexingIsNotStored, applied at startup. Data only: a corpus or chunk set
stored asindexingbecomesdegraded, once, and the next pass records what it finds.
It cannot be undone. Nothing re-chunks or re-embeds. - Whether a corpus is indexing is read from its jobs and no longer stored
(D-37). Everything that reports it
says the same as before, but a restart no longer marks an interrupted corpusdegraded:
it keeps the outcome of its last pass, and the job that was running readsfailedwith
Interrupted. A chunk set is storeddegradeduntil its first pass finishes and reads
indexingwhile that pass is queued or running. - The skill changes (
dexicon-skill-version: 4). Runningscripts/install-mcp.ps1again
upgrades installed copies. The hooks are unchanged. - A key can hold a new scope,
configure, which no existing key has and the key adopted from
DEXICON__BOOTSTRAP__TOKENnever does.PUT /api/tokens/{id}/scopesis new. Additive. - Refused where it was accepted before: a size cap of zero, which indexes nothing, when adding
a source or setting a corpus default; and a corpus name that matches another's ignoring case,
contains a colon or a control character, or is longer than 200 characters.
Added
- A
configurescope lets an agent set up what is indexed
(D-36). Three more
MCP tools are listed for a key holding it:list_folders,configure_corpusand
configure_source. With them an agent lists the mounted folders, creates a corpus, changes a
corpus's description and the filters its sources inherit, and adds or changes a files or
history source. Nothing over MCP removes a corpus, source, chunk set, document or key. Each
change goes through the service the UI uses and is logged with the key's name. A narrower
filter drops what it stops selecting when the refresh runs, as in the UI, and the replies
say so..gitignorecan be pinned on but not turned off over MCP, which is what keeps
Dexicon's own.envout of the index where the workspace root is its checkout. - A key's scopes can be changed in place, on the Access page or with
PUT /api/tokens/{id}/scopes, without issuing another key. A scope removed, or a key
revoked, is refused from the next request. index_statuswith a corpus named says why files are missing. Each source shows what it
reads and its effective filters, with which come from the corpus defaults. A history source
shows the ref it follows, how far behind its upstream it was, and its newest commit. The
failed, skipped and empty files of the default chunk set follow, ten per status with the
recorded reason on one line. Without a corpus named it keeps to counts.
Changed
- Corpus and source changes are judged in one service, which the UI and the agent tools
both call, so one cannot accept what the other refuses. - A corpus change is audited when it is saved, not after the request returns, so a failure
in between leaves an entry under the key's name. - A chunk set cannot be deleted while a job is working on it (409). Deleting the row
turned a job scoped to the set into one for the whole corpus. - Dependencies: vite 8.3.2, vitest and
@vitest/coverage-v85.0.3, lucide-react 1.51.0,
OllamaSharp 5.5.0, AngleSharp 1.8.3, andsource-map-js1.2.2 for GHSA-68fv-2mgg-jv7q.
Fixed
- A corpus could stay
indexing, and be skipped by the scheduled refresh, until a restart.
A full data disk refused every write for about four minutes. The jobs in flight could not
save their outcome, and the corpora they had markedindexingstayed so for six hours. A
job now tries that save up to five times, and since the state is read from the jobs and the
corpus lease it cannot be left set: a job that stopped stops counting once its lease lapses,
and the corpus is refreshed again with nothing to repair. - A job whose lease claim failed was left
queuedand absorbed every later refresh. The
failure was dropped with the corpus's own state, so the row readqueuedwith no error,
index_refreshreturned that job, and nothing ran it. It is now recordedfailedwith the
reason. A job that loses its lease while running records its outcome too, and leaves the
corpus to whoever holds it. - A name typed with different capitals or spaces made two corpora one name could reach, or
failed on the unique index as a 500. It is now refused as a conflict.
docker pull ghcr.io/merp4/dexicon:0.6.5
Digest sha256:42a70130293c155253211a0fbc9de526d5d7f42788c16a46883f3c0e38c06d4a. An SBOM and build provenance are attached to the
image in the registry.
v0.6.4
⚠️ Upgrading
- No migrations, and nothing re-chunks or re-embeds.
- A source added from now on, including one named when its corpus is created, follows the
corpus's.gitignoresetting and size cap until it sets its own. Sources that already
exist keep the values they hold: the UI and corpus creation stored both on every source,
so a corpus default for either reached none of them. To have one follow the corpus, open
Edit source and tick Corpus default or Server default beside the field. CorpusSummarygainsconfigured: the server's.gitignoresetting, its size cap, and
the separate cap for PDF, DOCX, PPTX, EPUB and HTML files. Additive.
Changed
-
Adding a source asks what to index first. Files or Commit history opens the
dialog, and each shows only its own settings: Which files (Include, Exclude,
.gitignore, Size limit), or Which commits and Each commit's document holds. It was a
checkbox after the folder picker that switched half the form's fields out. The add, edit
and default-filter dialogs share one set of fields. Each filter has a Corpus default
or Server default tick naming where its default comes from, and while ticked the
field shows that value. The size limit says it is for code and text files, and gives the
separate document limit. -
A corpus's sources line up in columns: name, count, settings and buttons, from the
smbreakpoint up. With thirteen sources, each row's buttons sat wherever its text
ended. A file source's row reads.gitignore respected · code and text ≤ 64.0 MB; the
bare≤ 64.0 MBsaid nothing true about the PDFs on a shelf of books. -
A run that re-indexed nothing says so. A refresh over an unchanged tree read
Succeeded · 37/37 documents · 0 chunkson the corpus page; it now readsSucceeded · no documents re-indexed, 37 checked. -
Dependencies: vite 8.3.1, jsdom 30.1.1, lucide-react 1.48.0, cn 0.4.0,
Microsoft.Extensions.AI.Abstractions and .OpenAI 10.10.1, coverlet.collector 10.1.0.
Fixed
-
A corpus created after the page loaded gets live progress.
/api/eventsread which
corpora the caller can see once per connection, so a new corpus's first index showed
nothing, and its page never re-read the counts when a run ended: a chunk set that had
built 102 chunks stayed at "indexing · 0 chunks", and Promote offered it as holding
0. What the caller can see is now read again on every heartbeat and on the first report
for a corpus outside it, and the newest report of such a corpus is written once it
becomes visible. A key whose mapping is narrowed stops receiving the corpus it lost
within a heartbeat, where it used to keep receiving it until it reconnected. -
The edit dialog's default for a size cap was the source's own value. A source
pinned at 64 MB offered "corpus default (64.0 MB)", where following the default gave the
configured 256 KB. -
A long source path wraps instead of widening the corpus page past the window, and an
uploaded-documents source is named as one rather than described with the file settings
(code and text ≤ 2.0 GB, from its stored cap).
docker pull ghcr.io/merp4/dexicon:0.6.4
Digest sha256:dd86d75ac9921c1a86f6cd39833d37947bde3df095af971376ec2495c770447f. An SBOM and build provenance are attached to the
image in the registry.
v0.6.3
⚠️ Upgrading
- One migration,
GitHistoryTracking, applied at startup. Widening only: one nullable
column onsources, filled by the next pass over each history source. Nothing re-chunks
or re-embeds. - The skill (
dexicon-skill-version: 2), the SessionStart hook and the hook library
(dexicon-hook-version: 2) change. Runningscripts/install-mcp.ps1again upgrades
installed copies. A copy whose version marker was removed is left alone; one edited with
its marker in place is replaced. install-mcp.ps1now registers Claude Code at user scope unless-Scopeis passed. An
install made under the old default put the skill and hooks in a project's.claude,
which-Uninstall -Scope project -Project <that repo>removes, and registered the MCP
server at Claude Code's local scope, whichclaude mcp remove dexicon --scope local, run
in that repo, removes.
Added
-
Choose what a history source follows from the repository's own refs. The history
editor's Ref box becomes Follow: the checked-out branch, a branch picked from the
repository's local, remote-tracking and prefetched refs, or another ref typed in for a
tag or a commit. Local branches say how far they are from their upstream as of the last
fetch ("52 behind origin/main"), the remote-tracking group says whengit fetchlast
ran, and a branch that is behind offers its upstream instead. A pick stores the full
name, so a tag of the same name cannot take its place; a stored short name shows as
picked as git resolves it, and one that resolves to a tag hiding a branch says so. The
refs come fromGET /api/workspaces/git?path=&ref=, which reads refs and FETCH_HEAD's
time and nothing else: no remote URL and no configuration. -
A history source's row says how current its ref was. Each pass records the branch
the ref resolved to, its upstream and the distance, and when a fetch last ran, in
sources.git_tracking, returned asSourceSummary.trackingfor the ref it was observed
for. The row readsHEAD (main), "52 behind origin/main as of the fetch 3d ago" in the
warning colour, or "fetched 3d ago" for a remote-tracking ref. A read that fails keeps
the last record and changes nothing else about the pass. -
Keeping a mounted repository current is the host's job, and docs/04 says how.
Dexicon does not fetch, and holds no credential for a remote. "Following a remote
without Dexicon fetching" coversgit maintenance start, whose hourly prefetch writesrefs/prefetch/
and never movesorigin/main, and a scheduledgit fetch, both with the host's own
login.
Changed
-
Agents are told to name the corpus that fits. With several corpora of different
kinds, a search across all of them lets hits from the corpora that cannot hold the
answer take the places of the one that can: a question about one project's design,
asked of five corpora, drew 6 of its top 10 hits from the four that did not hold it.
The skill has a direct/dexicon-searchsearch the one or two corpora whose
descriptions fit and widen only when that comes back empty or thin; the SessionStart
hook says the same when more than one corpus is listed; andsearch_index'scorpus
description says so to every MCP client. -
Claude Code is registered at user scope. Its project and local scopes are keyed by
the literal working-directory string, so a server added from one shell could be missing
from a session started in another. The Access dialog's ready-to-paste command and every
documented copy end in--scope user; the installer defaults to it for anything that
only touches Claude Code, and honours-Scope project, which it used to drop. A
project-scope.mcp.jsonordexicon-hooks.envholds the key in plain text, and the
docs and the installer say to keep it out of version control. -
The header wraps in reading order. Narrower than about 1,056px, its chips wrapped
inside the bar into two ragged rows; the nav and then the status group now take rows of
their own, in the order they are read and tabbed through. A chunk set's description
wraps beside its buttons instead of pushing them onto a row of their own.
Fixed
-
Hooks installed into a project read that project's configuration. The installer
wrotedexicon-hooks.envinto the project's.claude, and the hooks read
~/.claude/dexicon-hooks.envalone, so they read the user's file or none, and did
nothing without error. They now read the file beside theirhooksdirectory first. -
Adding a history source when git cannot be asked is a 503 with git's reason. A
missing git or a timeout came out as a 500. -
git runs in the C locale. Where git has translations, the
--statsummary line
that goes into each commit's document was translated. Neither the image's git 2.54.0
nor Git for Windows 2.31.1 translates, so nothing changes there.
docker pull ghcr.io/merp4/dexicon:0.6.3
Digest sha256:b99f95f78a4ba9196ec6d7460b7c9381810ecf0f44fcbb118f890ef17bcc414c. An SBOM and build provenance are attached to the
image in the registry.
v0.6.2
⚠️ Upgrading
- No migrations, and nothing re-chunks or re-embeds.
Security
- Signing out ends the session on the server. The UI cleared its token before the
sign-out request went out, so the request carried none, the server had nothing to revoke
and answered 204, and the session stayed usable until it expired. The request now names
the session it ends. A 401 for a session that is no longer the stored one does not sign
the page out, so a refusal that arrives late for an old session cannot end a newer one.
Added
-
A commit limit can keep what it indexed.
maxCommitsalone is a window: each new
commit pushes the oldest out of the corpus on the next pass.keepIndexed, off by
default and refused withoutmaxCommits, makes the limit a starting point: a commit
once indexed stays while the ref reaches it and the other settings still select it. A
source withkeepIndexedon lists the whole reachable history on every pass. The history editor
offers it beside a commit limit and warns before a save that turns it off, since every
held commit past the limit then leaves on the refresh. -
Each search hit shows its score, and Explain says how it was made: DBSF over the
dense and sparse lists for hybrid, cosine similarity for semantic, an IDF-weighted term
match for keyword. Explain said reciprocal rank fusion for all three, which hybrid
stopped using in 0.3.0.
Changed
-
Promote and Revoke ask first. Each acted on one click. Promote now names the set
search moves to and the one it leaves, with both models; Revoke says a revoked key cannot
be restored from the page. -
A passage marks what the keyword index matched. The terms are each word of the query
and the parts of a compound identifier, with the index's term lengths and stopwords, and
the passage is read token by token the same way. It marked whole query words as
substrings, so most keyword hits for an identifier had nothing marked, and a part could be
marked inside an unrelated word. Common words the query has on their own are still left
unmarked. -
Opening a hit marks all of it, wherever it is. The viewer marked only the first line,
and loaded only the first 400,000 characters of a file, so a hit further into a book was
neither marked nor scrolled to. It now reads on until the hit's last line is in. -
Chunk sizes on Documents carry their units, Access shows one badge per scope, an empty
status tab says "No failed files", and an inherited filter field is named on its line.
docs/08 describes the Search screen as it is; it listed controls the page never had.
Fixed
-
Live progress keeps arriving on a page left open. The progress stream lost one event
for every 20-second idle ping it had sent, so a page open for 20 minutes lost the next 60
or so: bars froze and a finished refresh never reloaded the corpus. The connection stayed
open, so nothing reconnected. -
Deleting a corpus counts every chunk set it deletes. It gave the default set's count.
-
The corpus page no longer says "Run a refresh" while its files load. It drew the
page with an empty list for one round trip on every visit. The corpus now also draws when
the file list fails, and a listing that answers after a newer one no longer replaces it. -
A source can be added at the workspace root. Its path is empty and the submit was
disabled for an empty path, so the coverage notice's button for a gap at the root opened
a form that could not be sent. A file source there now warns that it takes everything no
deeper source claims. -
"1 chunk", not "1 chunks", and the chunk-set summary's accessible name has spaces
between its parts.
docker pull ghcr.io/merp4/dexicon:0.6.2
Digest sha256:cc0e1a9a5a5c244aba8aeb66f7252a53a76d42a9786aeacda9ba7bbbe60833a7. An SBOM and build provenance are attached to the
image in the registry.
v0.6.1
⚠️ Upgrading
- One migration,
GitHistoryNewestCommit, applied at startup. Widening only: two nullable
columns onsources, filled by the next pass over each history source.
Added
-
A history source shows where its ref had got to. Each pass records the newest commit
it found, and the source's row readsnewest 9d2ef06, 3d ago, with the full sha and date
on hover;SourceSummary.newestCommitcarries both. A source followingHEADin a
checkout whose local branch nobody pulled indexed the same 174 commits for three days
whileorigin/maingained 52, with every count on screen correct and nothing to say the
history had stopped. It records where the ref points, not how far indexing got, which
the counts and the source's state report. An inventory that fails leaves the last
record; one whose settings select no commits clears it. -
A history source's settings can be edited. Its row had no editor, so changing the
ref meant the API, or removing the source and re-reading every commit. The editor holds
the ref, the message, stat and diff with its cap, merges, a commit limit, a date and the
include paths, and the add dialog now shows the same fields from the same component, so
a ref such asorigin/maincan be chosen when the source is added. The editor says
which kind of change it is about to save: the ref, limit, date and merges choose which
commits are indexed and keep the documents already there, while the message, stat, diff
and paths change every document and re-read the history.
Changed
- History settings git cannot be asked with are refused when a source is added or
edited. A malformed ref, a commit limit below one or a diff cap past the read ceiling
was stored as sent, answered 200, and found on the next pass as the source being
unavailable. It is now a 400 carrying the same reason the inventory gives, and the
add and edit dialogs show it, staying open. None of the checks runs git; a well-formed ref that names
nothing still shows on the corpus after the refresh.
Fixed
-
A dialog shows its own failures. Eleven dialogs sent a failure to the page's error
banner, which sits under the dialog's overlay and inside the part of the page Radix
hides from assistive tech, so a refused save left the dialog open with nothing in it to
say why. Measured with New corpus and a name already taken: the 409's message rendered
behind the overlay. The file and extracted-text viewers, on a load that failed, went on
saying "Reading…" for ever. Each now shows the server's message at its top, scrolled
into view if the dialog had scrolled past it, and stays open. A test that parses the
source holds every dialog to it. -
A source at the workspace root has a name. Its path is empty, and
rootPath ?? kind
passed the empty string through, so the row showed no name and its buttons were
labelled "Remove source " with nothing after it. It is nowworkspace root.
docker pull ghcr.io/merp4/dexicon:0.6.1
Digest sha256:84ec1e5a2fd010dfe390183152159516ee6db709ab03c5198e1c8d4ef867baae. An SBOM and build provenance are attached to the
image in the registry.