Commit Graph

1054 Commits

Author SHA1 Message Date
claude ab3e818bb9 A named destination silences the guessers and moves nobody (V-655)
querySources splits in two once you look at which sources over-claimed during
the week of 2026-08-07. The clean ones perform a lookup and can come back
empty: fact-by-key, tasks, list, money, calendar, notes. The dirty ones decide
by cosine against frozen seeds and then answer whatever they claimed, because
they have no lookup that could miss. Weather has no local table at all, which
is why "что такое TCP?" became "для какого города?".

So each source now carries its destination and whether it guesses, and
queryWalk takes the guessers that were not named OUT of the chain. It removes
and never reorders, which is the whole safety argument: the table's order is
load-bearing, every comment on it argues a reason between two sources, and
above all it carries "his data first, then the world". Naming SourceWorld does
not send the turn outside. It stops weather claiming a protocol on the way
past. His notes, his facts and the boundary in front of them still run first,
so a wrong destination costs nothing but the guess it prevented.

The skipped sources are recorded as never-asked with the reason, so /trace
shows a narrowed walk rather than a chain that silently shrank.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:02:13 +04:00
claude b5500a5be8 Say where the answer lives, not just that it is a question (V-655)
A question was sorted twice. The cascade picked one of seven intents with
stage 0 rules, the resident model and the classifier behind it, a 91-case
fixture measuring it and the decision trace recording it. Then IntentQuery
handed the turn to a second dispatch in the daemon, twenty-two branches
deciding by seed similarity in a fixed order, with none of that. The careful
sorter did the easy half.

Decision grows a Source: twelve destinations, not twenty-two, because the
recall passes are one destination from the outside and so are the three world
sources. Empty is a real value and it is the floor — nothing names one, the
daemon walks its whole chain, and that is exactly what shipped before.

Stage 0 fills it where a deterministic rule already knows. Two new world rules
for the shapes measured failing on the box on 2026-08-07: "что такое TCP?" and
"кто такой Линус Торвальдс?" were answered by weather and by the personal
boundary, and "сколько будет 17 на 23?" was answered "для какого города?".
The calendar noun rule and the closed event-noun rule name the calendar. The
possessive agenda rules deliberately do not: "что у меня в списке покупок"
matches agenda-query, and naming the calendar there would take the list off
the turn.

Fixture unchanged at 69/91, which is the point — it scores intent, and none of
these cases changes intent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:01:59 +04:00
claude 9095ac847d Merge pull request #198 2026-08-07 10:24:39 +02:00
claude 4b5f6adbae Merge pull request #197 2026-08-07 10:22:07 +02:00
claude ecb8ba72eb Write down the bound on suspension (V-654) 2026-08-07 12:16:23 +04:00
claude 4fdecf9a25 Let a question go after it has stepped aside three times (V-654)
A side query suspends the parked question rather than dropping it. Nothing bounded that. No attempt is spent, so MaxAttempts never applies, and noteSuspended restarts the 90s clock, so the TTL cannot arrive while he keeps talking. Measured 2026-08-07: one unfilled time slot rode the tail of six consecutive unrelated replies.

PendingQuestion.Suspends counts the step-asides, MaxSuspends is 3, and past it she lets the request go with the same clarifyDropped line every other drop uses. The count is of consecutive step-asides and resets the moment he answers.

Also splits the re-ask off the answer into its own sentence. The comma splice buried the question in the tail of a reply about something else.
2026-08-07 12:16:15 +04:00
claude 2bbd8edbf6 Record the week of usage that found V-654 and its siblings (V-654)
Two untracked files left in the tree by the audit session. They are the evidence behind V-654 and several sibling tasks, so they belong on master rather than inside the PR that fixes one of them. Dated eval files under docs/evals/, so they are never edited after the day.
2026-08-07 11:59:40 +04:00
claude beb093aebb Run one test and audit the repo without retyping either (V-653)
Two commands replace work that 66 sessions of transcripts show being
redone by hand.

`make t` replaces the CGO preamble, pasted 391 times across past
sessions and documented in CLAUDE.md as the way to do it. It also sets
MAVEN_ONNX_LIB, which that recipe did not: the four TestONNX*
measurements self-skip without it and the run still prints "ok", so
every targeted eval done the old way reported the hash ratchet while
reading as a real embedder score. -race keeps it honest against `make
test`, -count=1 keeps a stale cache from passing as a result.

`make audit` replaces the inventory sweep. The four longest sessions
spent 93 greps rebuilding it before their first edit. Runs in 0.75s.

Its stub search is narrower than the sweeps were, on purpose. "not
wired" is this repo's word for a nil dependency and matched ~30
comments describing working code; "placeholder" names real identifiers
and matched 16 more; internal/ipc/unimplemented.go is the deliberate
Unimplemented*Server pattern, not 60 gaps. A gap report that reports
the architecture back at you is one nobody reads twice.
2026-08-07 03:13:08 +04:00
claude b1b326018f Merge pull request 'NEEDS-KAMI: telegram is the only reach, and it depends on a socks relay that has failed before' (#196) from task/649-needs-kami-telegram-is-the-only-reach-an into master 2026-08-07 00:50:34 +02:00
claude 08889cad88 Give the box a second reach (V-649)
Telegram was the only way off this box, and it is not a direct path: it
needs api.telegram.org, a socks relay on the host and a matching ufw rule.
Each of those three has failed once, and when they do a sev4 nudge has
nowhere to go. ntfy shares none of them.

The spare is the smaller half of it. The routing table already sends
sev3-away nudges and away reminders to ntfy and to nothing else, so with no
block configured those two routes hit a nil sink in DispatchNudge and
DispatchReminder and are skipped — no log line, no delivery_attempts row.
An away reminder is worse than dropped: out stays empty, so MarkReminder
never runs and it re-fires every tick without ever being delivered.

Owner's call, 07-08-2026: ntfy.kvmx.ru, topic maven.

The sink now takes a bearer token, which is what that server wants and what
it could not do before. ntfy scopes a token to one topic and to write-only,
so a popped sink can push to the maven topic and cannot read it back. Basic
auth stays for a server with no tokens; configuring both is refused rather
than resolved by guessing.

Config keys got json tags. docs/operations.md has documented this block as
base_url/topic since before it existed, and the untagged struct would only
have answered to BaseURL/Topic — the documented config would have parsed
into an empty one.

The token is a ${NTFY_TOKEN} expansion from the gitignored
deploy/telegram.env, beside the telegram secrets. TestDeployConfigLoads now
fails if the block goes missing, because deleting it is how you turn the
reach off and the two silent routes are what that costs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMNNEkYx1mZFtHNrFk7uqb
2026-08-07 02:16:18 +04:00
claude a4630b9314 Merge pull request 'MemoryStore.Search decodes and unmarshals every row before keeping topK' (#195) from task/643-memorystore-search-decodes-and-unmarshal into master 2026-08-07 00:09:50 +02:00
claude 39d44bb384 Close a Vikunja task with done, and nothing else (V-641)
Owner's call, 07-08-2026. A completion summary written into the
description on the way out is lost anyway, and the durable record is the
commit messages and the merged PR.

Written during the V-641 session and left uncommitted; it rides this
branch rather than being dropped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMNNEkYx1mZFtHNrFk7uqb
2026-08-07 01:48:19 +04:00
claude 65ee0f9c61 Score every row, pay for only the ten that survive (V-643)
Search decoded the vector blob into a []float32 and JSON-unmarshalled the
meta map for every row, then sorted all N and threw away everything past
topK. Meta only ever matters for a survivor, and the sort answered a
question a bounded heap answers cheaper.

The scan still visits every row — that is what picks the winners. What it
no longer does is allocate for a row it is about to discard. dotBlob reads
the vector out of its stored bytes, so scoring costs nothing; a row is
copied and its meta unmarshalled only once it has entered the topK.

At 10000 rows and topK 10: 70.6ms to 26.8ms, 58MB to 17.5MB, 240k allocs
to 60k.

Recall is unchanged where it is measured. recall+onnx scores 22/32 with
recall@1 70.4% and recall@3 85.2%, identical to before.
TestMemoryStoreSearchMatchesNaive pins the ranking against the full-sort
implementation it replaced, and TestDotBlobMatchesDot pins bit-identical
scores, which the 0.008 gate margin demands.

One behaviour did move: ties. sort.Slice is not stable, so equal scores
were ordered arbitrarily; the heap now keeps the earliest. Under the real
embedder an exact tie is a duplicate vector and nothing moved. Under the
hash embedder the eval's floor uses, everything ties at 0 and that run's
recall@3 went 74.1% to 81.5% — a number that measures tie order, not
retrieval. recall@1 and false recall, the two the eval asserts, are
unchanged on both runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMNNEkYx1mZFtHNrFk7uqb
2026-08-07 01:48:05 +04:00
claude 76938e206d Put a number on the recall scan before changing it (V-643)
MemoryStore.Search is on the per-turn recall path and had no benchmark, so
any claim about its cost was an argument rather than a measurement.

Seeds a store with rows the shape recall actually stores — 384-wide
vectors, the resident embedder's width, and a meta blob carrying the note
text — at 1000 and 10000 rows. 10000 is the ceiling the type doc claims a
full scan is fine at.

Measured as it stands: 5.3ms and 24k allocs at 1000 rows, 70.6ms and 240k
allocs at 10000.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMNNEkYx1mZFtHNrFk7uqb
2026-08-07 01:48:05 +04:00
claude 0b3d81ecbf Merge pull request 'Two maps grow for the process lifetime with no eviction' (#194) from task/641-two-maps-grow-for-the-process-lifetime-w into master 2026-08-06 23:33:23 +02:00
claude 4be6852b94 Drop host rate-limit entries that can no longer delay anything (V-641)
webfetch.Fetcher.last held one entry per distinct host the crawler ever
dialed, never pruned. Bounded in practice by how many hosts get crawled, but
crawl.on_demand is true in deploy, so the host set is whatever he names out
loud.

An entry older than HostInterval cannot delay a request — waitTurn would let
the next one straight through — so it is dropped. The sweep runs on write and
only once the map passes 64 entries, below which walking it costs more than
the entries do.

Rate limiting is unchanged: a host dialed inside the interval is kept, which
the test asserts, because pruning one would hand out a free turn.
2026-08-07 01:32:26 +04:00
claude f7b76c572f Bound the undated-item set per feed (V-641)
rss.Poller.seen held every undated item ever seen, one entry per id, for as
long as mavend ran. fresh() added and nothing removed. A feed that ships items
with no <pubDate> grew it forever.

seenIDs is the same set with a bound: the map answers the lookup, a slice
remembers insertion order, and the oldest id falls out past 512. The cap has
to stay above any one feed's front page or an item still listed there would be
written a second time, and a few hundred covers the largest page anyone
publishes. The set only ever had to span one poll window plus the resync
guard, not all of history.

Dedupe behaviour is unchanged. The comment at fresh() explains why the set
does not survive a restart; it never bounded it within one run.
2026-08-07 01:32:15 +04:00
claude 05ddc5c92e Merge pull request 'mavcaldav is built, documented as running, and deployed nowhere' (#193) from task/644-mavcaldav-is-built-documented-as-running into master 2026-08-06 23:22:18 +02:00
claude b55e68f98d Say in compose that the calendar is off, and why (V-644)
mavcaldav was built, in `make build`, listed in CLAUDE.md's daemon table, and
deployed nowhere. Not commented out the way mavmaild is, which at least
records the decision and the enable steps. Built and mentioned nowhere is the
worst of the three states, so this writes the decision down.

The box has no CalDAV account, so the block stays commented. It names what the
absence costs, because both costs are invisible from the daemon table. Agenda
questions route correctly and answer from nothing: stage 0 sends "что у меня
сегодня" to IntentQuery (V-498) and the calendar query source then reads facts
nobody writes. And loop.State.CalendarBusy is fed by those same facts, so the
gate's "do not nag mid-meeting" is permanently false.

CLAUDE.md said the absence was an oversight. It is a decision now.
2026-08-07 01:19:44 +04:00
claude beaa24754c Read the CalDAV password from a file, not from argv (V-644)
mavcaldav took -pass and -render-pass as flag values, so enabling it would
have put his calendar password in `ps` inside the container, in the compose
file, and in shell history. mavpoll and mavmaild both read their secret from
a file for exactly that reason.

readSecret reads once at start, trims, and refuses an empty or missing file.
An empty file is a deployment mistake, not a password, and basic auth would
otherwise send "" and collect a 401 every poll. A rotated password means a
restart, which is cheaper than re-reading the credential every five minutes.

Nothing called the old flags: no compose service, no systemd unit, no test.
So they are replaced rather than kept beside the new ones.
2026-08-07 01:19:33 +04:00
kami aed8cac439 Merge pull request 'The store caps sqlite at one connection under WAL, so every read queues behind every write' (#192) from task/642-the-store-caps-sqlite-at-one-connection into master 2026-08-06 23:04:39 +02:00
claude af4eeceb6a Keep the store's one connection, delete the seam it cannot survive (V-642)
`SetMaxOpenConns(1)` under WAL gives up concurrent reads, and the task
asked whether that costs anything. Measured over a fixed two-second
window, a paced writer against a read loop, three runs per cap:
reads do not queue. Four connections buy 70µs at p50 on a turn that
spends 1.19s in the resident model, and write throughput more than
halves. A 19ms worst case also cannot be the source of the 2.7s router
figure, so that line of enquiry is closed.

What the cap cannot survive is a long-lived transaction. It holds the
only connection, so a second read never completes: two seconds and
`context deadline exceeded`, against 1ms at a cap of four.

`Store.DB` handed out exactly that transaction. It had been there since
the initial commit with no production caller, and its comment described
a loop that never materialised. Its one user was a test helper reading
`delivery_attempts` by raw SQL, which `ListDeliveryAttempts` has covered
since V-390. So the cap stays and the seam goes, and the hazard is gone
by construction rather than by documentation.

`internal/store/conncap_test.go` stays as the standing measurement,
skipped under -short. The comment at the cap and the one in
`internal/ipc/server.go` that leans on it now state the invariant and
cite the numbers.

Measurement: docs/evals/2026-08-07-store-connection-cap.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 01:01:27 +04:00
kami 7b507dec94 Merge pull request 'factEnrichmentWorker walks the pending queue twice per tick to write one log line' (#191) from task/647-factenrichmentworker-walks-the-pending-q into master 2026-08-06 22:33:54 +02:00
claude 2c0334c4fe Count the enrichment backlog without a second query (V-647)
`tick` read `PendingFactResolutions` at the scan limit, then `status`
read it again with the same limit for one log line. Up to 2000 rows per
tick on a database that serialises reads, to say how long the queue is.

`statusOf` counts over a batch the caller already holds, and the tick
passes it the batch it just read. A resolved fact leaves the queue, so
the loop collects what is still pending rather than reporting the
pre-tick count. `status(ctx)` stays as the querying form, for a caller
outside the tick with no batch in hand.

No behaviour change: the three counts still describe one row set, and
the same facts are attempted per tick.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:32:54 +04:00
kami 92cbdbfdd3 Merge pull request 'V-637 follow-up: telegram intake has no deploy switch, and the chat-id check cannot fail a boot' (#190) from task/646-v-637-follow-up-telegram-intake-has-no-d into master 2026-08-06 22:18:25 +02:00
claude e78b2d8992 the daemon table, against make build and compose (V-648)
The table listed nine binaries. make build builds eleven, and mavseal and
labelgen exist without targets. The running count said seven on homesrv;
docker-compose.yml runs five.

Adds mavgpud, mavupdate, mavseal and labelgen, and names why each absent daemon
is absent: mavmaild has no mail account, mavwaked and mavenclient belong on
workpc, and mavcaldav is an oversight (V-644).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:14:42 +04:00
claude 9d58922462 Refuse a telegram intake chat id the poller cannot match (V-646)
The push half accepts an @channelusername and the intake half cannot: an
inbound update names its chat by number, so an @-name matches nothing. The
check lived in NewPoller, which wireTelegramIntake logs and returns from, so a
box configured that way booted clean with a dead intake half and a working push
half. Nothing looked broken from the chat.

ValidateIntakeChatID moves the rule where config validation can reach it, the
same shape validateNetScan uses. It is stricter than the old prefix test: any
non-digit is refused, not just a leading @. An empty token or chat id still
means telegram is not wired, because an unset ${TELEGRAM_*} expands to empty
and that must not fail a box with no bot.

deploy/mavend.json turns intake on. The chat id on this box is numeric.

The onCallback comment claimed every path answers the callback. The fromOwner
early return does not, and silence toward a stranger is correct, so the comment
was what was wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:14:42 +04:00
claude b5ac48c126 One boot path for the workers and the API (#189) 2026-08-06 21:54:13 +02:00
claude 69d0f5ee78 No deadline survives the turn path, from mavweb down to llama-server (#188)
Co-authored-by: claude <no-reply@agents.claude.kvmx.ru>
Co-committed-by: claude <no-reply@agents.claude.kvmx.ru>
2026-08-06 21:11:42 +02:00
claude 661b5c1099 the audit write-ups, so every agent starts with them (V-638)
A repo-wide sweep on 06-08-2026 at 06c1cf2. Three docs, three tasks.

docs/plans/24-no-deadline-on-the-turn-path.md (V-638). Nothing between a
mavweb handler and llama-server can be cancelled, and one hop has a timeout.
Replier takes no context, the ipc client sets no conn deadline and checks ctx
once, and the ipc server dispatches under Background. Four commits, and the
pattern to copy is already in internal/voice/client.go:101.

docs/plans/25-the-two-boot-paths.md (V-639). The passkey-unlock path starts
seven workers outside the WaitGroup that shutdown waits on, shadows that
WaitGroup at main.go:529, and builds a daemonAPI with no nexus and no
getMCPServers. Latent, because db_key_env means the box boots unlocked.

docs/evals/2026-08-06-routing-trajectory.md (V-464). The deterministic path
and the cascade now score the same 69/91, and the cascade has not been
re-measured since V-626 and V-627. Either the model still earns its place or
it is costing 1.17s a turn for nothing. Dated, so it is not edited later.

Committed with --no-verify, on the owner's instruction of 06-08-2026. The
pre-commit hook refuses master and the alternative was three PRs for three
markdown files. Markdown is already exempt from the size cap for the same
reason: docs land as one batch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:41:03 +04:00
claude ff70637a0d Merge pull request 'Inbound telegram: turns and corrections from the chat' (#187) from task/637-inbound-telegram-turns-and-corrections-f into master
Inbound telegram (V-637)
2026-08-06 19:01:59 +02:00
claude 06c1cf247e the intake allowlist has to be a numeric chat id (V-637)
Two defects my own review found.

The push half accepts @channelusername as a destination. The intake half
cannot: an inbound update names its chat by numeric id, so that config would
read the chat, match nothing, and answer none of it. Refused at NewPoller,
which turns a dead reach into a line in the log.

And getUpdates returns at most 100 updates per call, so one call was not the
backlog. The skip loops, bounded at ten rounds rather than until empty, so
an instance that keeps handing back a full batch cannot spin.
2026-08-06 21:01:16 +04:00
claude 400653810e telegram is no longer outbound only (V-637)
The correction gesture now reaches all three surfaces, and CLAUDE.md said
only /chat had it. Doc 23 carries the decisions: long-poll rather than a
webhook, the backlog dropped on start, one accepted sender, and the two-tap
keyboard.
2026-08-06 20:59:03 +04:00
claude b3936348f5 gofmt the act target guard (V-634)
Landed unformatted, so make test failed on fmt-check for everyone after.
2026-08-06 20:55:53 +04:00
claude c61b0b3968 wiring the poller into both boot paths (V-637)
It reaches the daemon through ipc.CoreAPI and nothing else, so a telegram
turn takes the path POST /api/chat already takes: Chat returns the reply and
the trace id it collected off the context (V-630), and CorrectTurn writes
the label. Nothing in internal/delivery learns what a handler is.

Wired on the unlocked start and on the passkey unlock, like the mail intake,
so telegram behaves the same either way. A sink that will not build is
logged rather than fatal here, because wireDispatcher already failed the
boot on the same config.
2026-08-06 20:53:48 +04:00
claude 0a5211b038 tests for the inbound telegram poller (V-637)
The cases that matter: the turn runs with the chat as its dialogue id, the
reply carries the gesture, a turn nothing persisted carries no buttons, a
stranger gets no answer at all, the first tap writes nothing, and a write
that failed says so on the button instead of going quiet.
2026-08-06 20:53:48 +04:00
claude 38be702188 a fake bot API to test the poller against (V-637)
An httptest server that hands out one batch of updates per getUpdates call
and records everything else, plus a recorder for what the poller asked the
daemon to do.
2026-08-06 20:53:48 +04:00
claude d42372e996 the poller reads one chat and answers in it (V-637)
Long-poll getUpdates rather than a webhook: the box takes no inbound
connections and reaches telegram through a relay, so the direction has to
stay outbound. A failed poll waits and retries, because the relay going
down is the normal cause and it comes back on its own.

The backlog is discarded on start. Telegram holds undelivered updates for
24 hours, so a daemon that was down overnight would otherwise answer every
question in order, and a reminder set from an eight-hour-old message lands
at the wrong time. Missing it is the safe direction.

ChatID is the only accepted sender and anything else is dropped without a
reply, because a reply confirms the bot exists and whose it is. Chat ids are
not guessable but they are not secret either, so that is the whole
authorisation and it is an allowlist of one.
2026-08-06 20:52:34 +04:00
claude 45231ba69e the bot API calls the inbound half makes (V-637)
getUpdates, sendMessage, answerCallbackQuery and editMessageReplyMarkup,
plus the inbound shapes cut to what the poller reads. Every error goes
through the sink's redaction: the token is in the URL path because telegram
accepts it nowhere else, and net/http prints that URL on a transport
failure.

Only ok=true is a success, the same rule the push half already applies. A
relay that is up but cannot reach api.telegram.org answers 200 with an HTML
page of its own, and reading that as a batch of updates would be silent.

A chat id arrives as a number for a user and a string for a channel, so it
is held as json.Number and never converted.
2026-08-06 20:52:34 +04:00
claude 42c7b8b927 the correction gesture, as two taps in a chat (V-637)
Config gains an intake flag, off by default, and sendMessageReq gains the
inline keyboard the intake half hangs under a reply. The gesture itself is
the web's, ported: one button says the turn was wrong, and it opens the
seven intents rather than writing the negative straight away, because the
target is worth much more and he must still be able to decline naming one.

Button data comes off the wire, so parseCallback refuses an id it cannot
parse and a target that is not one of the seven. A label nothing can score
is worse than no label.
2026-08-06 20:52:19 +04:00
claude e5a1db995d Merge pull request 'Correcting a turn from telegram and from voice (V-628)' (#186) from task/636-correcting-a-turn-from-telegram-and-from into master
The voice half of the correction reach (V-636)
2026-08-06 18:22:28 +02:00
claude d32eae8aac a spoken correction lands in the label table, with or without a target (V-636)
The gesture was web-only, so the sample was skewing to the turns he happens
to type. Voice is where the hard cases are.

Half of it already existed: the repair rung has read "нет, это была заметка"
since V-455. It taught the classifier and wrote no durable label, so the two
paths disagreed about what a correction is. It now writes both. Two sinks and
not one on purpose: the classifier seed makes the next turn better today, and
the label is what a fitted head trains on after the transcript expires.

The trace id is stamped onto the remembered turn after the fact, because the
trace is written when the turn ends and recordTurn runs in the middle of it.

New: the untargeted half. "нет, не так" writes the negative and redoes
nothing, because there is no target to redo it as. Voice needs this more than
the web does — naming an intent aloud means saying "заметка" or "факт",
which is her vocabulary and not his.

repair_negatives is a new closed lexicon set matched against the WHOLE
utterance, never as a substring. That is what keeps it apart from
repair_markers, where "это не" is a fragment that needs an intent word after
it. A member that could appear inside an ordinary sentence does not belong in
the set.
2026-08-06 20:12:19 +04:00
claude 63b645b405 Merge the act target guard (#185) 2026-08-06 18:06:37 +02:00
claude 0e82cb442f the unplaceable word rides a typed error, not the message (V-634)
Recovering it by cutting on quotes in err.Error() meant the reply depended on
the wording of an error string. UnknownTargetError carries the word and
errors.Is still holds.
2026-08-06 20:06:25 +04:00
claude d94ed2e630 an act with a target the system cannot have does not run (V-634)
V-633 gave tools spoken aliases, so a Russian act reaches a tool. It resolves
the verb only: the rest of the sentence became argv. "перезагрузи роутер" ran
as systemctl restart роутер, which is a real tool, a real word and a target
that cannot exist on this box. She then reported systemctl's own confusion as
if she had tried something sensible, and on a destructive row she spent a
confirm turn on it first.

The executor now refuses, ahead of the confirm gate, and names the word it
could not place. The check is the script and not a word list: a unit, a
container, a host and a path are ASCII here, so a Cyrillic argv element means
the alias match swallowed the verb and handed on the next word.

Process rows only. An MCP argument is not a target — a task title is Russian
and always was — and a house row drops the spoken args already.

It does not try to guess the right target. Identity is Nexus's, and a target
Nexus resolves reaches Hexis through handleHexisAct before this executor is
asked.
2026-08-06 20:05:34 +04:00
claude c8f74c39d6 Merge the one-gesture correction (#184) 2026-08-06 17:51:04 +02:00
claude 44b8793e2f the plan says the gesture is gated (V-630) 2026-08-06 19:48:40 +04:00
claude a4b4733767 the correction gesture is step-up gated after all (V-630)
Trace ids are sequential integers and the label table is the one thing the
routing heads will be fitted on, so an ungated POST let anyone past the
transport gate mislabel turns the owner never touched.

The cost argument for leaving it open does not hold: he tapped to send the
turn he is correcting, so the session is already up when the buttons appear.
2026-08-06 19:48:29 +04:00
claude 8f168ab811 the routing trace section names the correction gesture (V-630) 2026-08-06 19:46:39 +04:00
claude eb129c2fad the correction, written down (V-630) 2026-08-06 19:46:22 +04:00