Commit graph

858 commits

Author SHA1 Message Date
3486932cbe
refactor(app): the payload envelope moves into halkyon_app (ADR 0030, D15)
Learn's PayloadEnvelope moves unchanged into halkyon_app, so that Train
seals its rows with the same envelope; Learn imports it from there. It
gets tests of its own: a payload sealed for its table, row, column and
item opens there alone, a changed one or one under a key not open never
does, and a readable one is trusted only signed by the account where it
was signed for.

Learn's app tests haven't run with the move (its build needs clang,
missing on this machine); analysis passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:52:46 +01:00
ff91f62303
docs(adr): ADR 0030's D13–D17, how Train records
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:52:46 +01:00
f749acb9ec
build(learn): the F-Droid repository keeps the kingfisher as its icon
The repository is all of Halkyon's apps', so its icon stays Halkyon's
emblem (ADR 0031, D3), kept in tool/release/fdroid/ now that the snap's
icon is Learn's graduation cap. Learn's listing keeps the snap's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:52:45 +01:00
ece9ce9697
feat: the apps' icons as one family, drawn by one tool (ADR 0031)
tool/app_icons.py at the root replaces Learn's and Todo's scripts: each
app's Lucide glyph (lucide-static 1.50.0) drawn by librsvg, fading from
cobalt through the colours ADR 0031 names, then every platform's
launcher icon through flutter_launcher_icons, Windows' .ico with every
size, and Learn's snap icon.

Learn's icon becomes the graduation cap, its window's on Linux too, with
Lucide's notice in its licence list; the kingfisher stays the emblem on
the rail and the launch screen. Todo's becomes list-todo, Train's bike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:52:45 +01:00
7b5fbee913
feat(train): Halkyon Train's shell, Todo's copied and renamed (ADR 0030, D12)
apps/train with halkyon_train_server, _client and _flutter, as Todo's:
halkyon_base configured by trainBase(), its own register alone and a
member of the instance's where the operator names one, the app on
halkyon_app's gates with Train's frame, account sheet and About, texts
in British English and German that speak of workouts, app.halkyon.train
on Android, iOS, Linux, macOS and Windows.

Where it differs from Todo: development ports of its own (8180 to 8190),
so it runs beside Learn's server; one initial migration with the schema
Todo's migrations end at (the same tables, compared line by line), not
Todo's seventeen; and Todo's icon as a placeholder until the icon tool
of ADR 0031 draws Train's bike. The workspace, the licences, the READMEs
and AGENTS.md name it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:52:45 +01:00
2fd39352fc
docs(adr): ADR 0030 (Halkyon Train) and ADR 0031 (the apps' icons)
ADR 0030 records what Train does and where its data comes from: running,
cycling and rowing, in the order recording, intervals, routes, music; the
Polar Loop taken over through Polar's BLE SDK; music from Jellyfin;
devices on phones through flutter_reactive_ble; Health Connect both ways
through the health package; every row sealed; the WP 5000 an FTMS rower,
as scanned; no WaterRower USB driver for now; the Loop's firmware from
Polar directly or through the instance; flutter_map, the instance's map
source, BRouter as a sidecar, offline use and working with CoMaps; and
Train's shell after Todo's.

ADR 0031 gives the apps' icons one family: Lucide glyphs fading from
cobalt through a colour between into each app's own, Learn's graduation
cap, Todo's list, Train's bike, Meditate's circle-dot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:52:44 +01:00
a0f2ea8de5
fix(base): the test of resets shown to the owner's devices asks the sign-in ways on its own clock
All checks were successful
CI / check (push) Successful in 36m43s
CI / deploy (push) Successful in 2m13s
Its clock is fixed at 20 Oct 2026 and the reset it asks lapses a week
after taking effect, so asked on the real clock it was gone from 30 Oct.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-30 15:52:36 +01:00
fe2b4389b7
fix(base): the test of a reset taken effect asks the sign-in ways on its own clock
All checks were successful
CI / check (push) Successful in 35m57s
CI / deploy (push) Successful in 2m21s
It read the real clock, on which the reset had lapsed a week after taking effect,
so it failed from 27 Oct 2026.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-29 15:51:21 +01:00
3e24bf8d9d
docs(deploy): deploy's manual key is pinned to redeploy.sh too
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-29 00:17:11 +01:00
1233731b62
chore(release): learn-v0.22.0
All checks were successful
CI / check (push) Successful in 35m9s
CI / deploy (push) Successful in 55s
learn-v0.22.0
Build 25.

Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 17:39:52 +01:00
5d95d41ef1
docs(adr): ADR 0021's D57–D63 as built
All checks were successful
CI / check (push) Successful in 36m13s
CI / deploy (push) Successful in 2m18s
How the budgets, handles, the operator's screens and effort read where the
decisions left something open: optional limits, money stopping only
priced assistants, a handle typed whole as one suggested, 50 at a time
with no search where lists stay short, lists changed in place after an
action, the effort's ceiling on output, and own keys without effort.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 16:21:54 +01:00
8b5bdfed8c
feat(learn): choosing how much an assistant thinks, and the assistants by provider in the field
Halkyon Learn's ADR 0021, D60 and D63:

- A chip after the scope's offers the efforts the operator allows for the
  assistant asked, its own marked; the one picked is kept per assistant on
  the device (DeviceSettings.askEfforts) and asked at, also by a
  conversation continued from the history. One the operator no longer
  allows counts as none.
- Every question asks for its effort's ceiling on output, as the thinking
  counts towards it (8,192 to 65,536 tokens; 16,384 at the model's own),
  where it asked for 4,096; the flashcards proposed, Explain and Why? ask
  at the assistant's own effort, a person's own key at none and 4,096.
- The field's picker lists the assistants under their providers' names,
  each in its colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 16:21:54 +01:00
cc87120bc8
feat(app): the operator's overview of screens, grants to several people at once with one budget each, assistants by provider
Halkyon Learn's ADR 0021, D57 to D63, on halkyon_base's budgets and
effort:

- The operator's screen is an overview (Waiting, Operators, Accounts,
  Assistants, each with its count) whose rows open screens of their own,
  read 50 at a time as they scroll (OperatorListCubit, OperatorPage), so
  hundreds of accounts push nothing else down (D62).
- A handle is picked among the accounts there are (AccountSuggestionField):
  appointing an operator and granting take no other (D61).
- AssistantGrantsScreen replaces AssistantGrantsCard: the assistants set
  up by provider, with their efforts and prices; the people granted, found
  by their handle's start; one sheet granting several assistants, a
  provider's at once, to several people, each with one budget in money and
  tokens a day and a month, or changing one person's (D58, D59).
- Assistants are grouped by provider wherever they are listed, each under
  its name with a dot in its colour (ProviderLabel, D60).
- AssistantsCard shows what is left of the person's budget for all their
  assistants together; the months of the figures run oldest first (D57).
- AssistantChoice says the efforts a question may ask at and the ceiling
  on output each asks for (D63); a person's own key gets none and keeps
  4,096 tokens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 16:21:53 +01:00
24ed63585e
feat(base)!: one budget per person for all their assistants, the effort each takes, the operator's lists by page
A grant now says only which assistant a person may use, and keeps their
agreement; the limits are the person's (assistant_budget): one per UTC
day and month in money and in tokens, each optional, money counting the
assistants with prices, tokens every one (Halkyon Learn's ADR 0021, D58,
D59). assistantOperator.grant gives several people several assistants
with one budget, change sets one person's exactly, revoke takes back
everything; grantees and granteesOf list people with their assistants,
budget and use, 50 at a time. The migration carries each person's grant
limits into a budget: per unit, their sum, a grant without a limit making
it none.

assistants.env gives each assistant a default and a highest effort
(_EFFORT, _MAX_EFFORT; refused at start where the provider takes none);
ask takes one within that range, refusing another (invalidEffort), and
caps the output at its ceiling (D63).

instanceOperator.waiting and accounts come 50 at a time from an offset,
and counts gives the rows of the operator's screen (D62).

BREAKING CHANGE: the offer, the grants and the operator's lists changed
shape; Learn's server now asks for build 25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 16:21:53 +01:00
0590a4b43d
feat(llm): a request carries how much the model thinks, in each provider's field
LlmRequest.effort (LlmEffort: low, medium, high, xhigh, max, Anthropic's
names) goes as Anthropic's output_config.effort, as reasoning.effort to
the Responses API and OpenRouter, and as reasoning_effort to OpenAI's chat
completions, Gemini and any other host. endpoint.efforts says which
levels a host takes (Gemini low to high, Mistral none); send refuses
another before anything goes, the fake client too. LlmEffort.outputCeiling
gives the room for output each level wants, as thinking counts towards
the ceiling (Halkyon Learn's ADR 0021, D63).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 16:21:53 +01:00
33c9a7df1c
docs(adr): ADR 0021's D57–D63, the operator's screen, budgets and effort
After the first use on learn.halkyon.eu, the developer decided how the
operator's side scales and how much an assistant thinks: the figures'
months oldest first (D57); a budget per person covering the assistants
granted, one sheet granting several people several assistants (D58,
amending D3); the budget in money and tokens, each optional (D59);
assistants grouped by provider, each in its colour, no logos (D60); a
handle chosen among those that exist (D61); the operator's screen an
overview of screens, loaded 50 at a time (D62); and effort within the
operator's range, chosen by the person (D63).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 16:21:53 +01:00
3f8c4c6c09
docs(deploy): the example's prices are Claude Sonnet 5.5's
The example names claude-sonnet-5-5 as its model but priced it at $3
and $15 per million tokens, older Sonnets' prices; Sonnet 5.5 costs $2
and $10 (Anthropic's pricing page, 26 Oct 2026). Limits in money would
have run out a third early.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 16:21:53 +01:00
19dd4d9c4e
fix(learn): the instance asks LanguageTool for its languages again a minute after it didn't answer
Switching the sidecar on learn.halkyon.eu's server recreated the server
beside LanguageTool, whose start takes a minute: the server's first look
at the languages failed, and it looked again only an hour later, as
after one that worked. Until then instance.info() named no languages,
and an app that asked meanwhile keeps that answer, so its editors check
nothing until it starts anew. A look LanguageTool doesn't answer is now
tried again a minute later, by a timer, until it answers, the languages
known before kept meanwhile; one that worked still counts for an hour.

The server's tests take fake_async (^1.3.3, as Learn's app, already in
the lock) for the timer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 16:21:52 +01:00
db7f6c0ab2
docs(deploy): the sidecars' root steps name deploy's folders in full
Step 1 of both sidecars changed into deploy's home before its sudo
commands, but Ubuntu closes a home to other users, so the operator's
own user, the one with sudo, gets "Permission denied" there (found
setting up LanguageTool on learn.halkyon.eu's server, 26 Oct). sudo
reaches the folders by their full paths, which the shell makes of
~deploy without entering it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 16:21:46 +01:00
7b4ce3fe29
chore(release): learn-v0.21.0
All checks were successful
CI / check (push) Successful in 33m45s
CI / deploy (push) Successful in 49s
learn-v0.21.0
Build 24.

Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 11:50:40 +01:00
973063a225
build(learn): the server's image holds every package it resolves
All checks were successful
CI / check (push) Successful in 33m55s
CI / deploy (push) Successful in 2m20s
The deploy of 46be9983 failed while building the server's image: the
Dockerfile copies halkyon_core since ADR 0021's step 1 (a060fbc2), but
.dockerignore still left it out of the build's context, and halkyon_llm,
which the module forwards the operator's assistants with since step 2,
was never copied at all. Nothing was switched: the instance kept running
v0.20.0's server.

CI never builds the images (its runner has no Docker, by design), so
test/deploy/images_test.dart now holds each Dockerfile to its package's
path dependencies, dev dependencies included as pub resolves them: every
one copied, kept in the narrowed workspace, and not left out by
.dockerignore. It failed on both before this fix. Built locally since:
the server's image builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 11:01:29 +01:00
46be998329
fix(learn): an editor with nobody signed in above it checks nothing
Some checks failed
CI / check (push) Successful in 10s
CI / deploy (push) Failing after 7s
The full window editor is also shown without a person's scope, as a
privacy test pumps it, where GrammarHost asserted for want of one.
halkyon_app's PersonScope gains maybeOf beside of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:11 +01:00
c23c0c80a1
docs(adr): ADR 0024, LanguageTool as a sidecar
The developer's decisions of 26 Oct (D1-D5) and the calls made
building it; notes in ADR 0017 (point 10) and ADR 0021 (D54) on what
it amends; Learn's AGENTS.md on the sidecar and the checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:11 +01:00
d2eaef255e
feat(learn): spelling and grammar checked as people write (ADR 0024)
Where the instance's LanguageTool checks the language, the editors
underline what it finds after a pause, a paragraph at a time, a tap
offering its suggestions (GrammarHost, GrammarCheckController,
GrammarOverlay; nothing written into the document). A card's front is
checked in the studyset's front language, its back and answers in the
back's, everything else in the language recognised.

In a private studyset nothing is sent until the person agrees for it,
asked once in a banner above the editor and kept in their settings
(an agreement under privateAgreements, a refusal in grammarDeclined,
settings schema 14), taken back in the studyset's privacy dialog.
Settings > Writing turns it off on the device.

The field's Check asks LanguageTool first where it checks the
studyset's language, in a sheet offering to ask the assistant why;
elsewhere it asks the assistant as before (amends ADR 0021, D54).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:10 +01:00
f766a880ac
feat(learn): LanguageTool as a sidecar, reached through the server
compose.languagetool.yaml runs LanguageTool's open-source server
(meyay/languagetool 6.8-13, pinned by digest) on `edge` with no site and
no published port, as user 61081 on a read-only filesystem, without a
log. The server alone reaches it (LANGUAGETOOL_URL): grammar.check is
for those signed in, takes 20,000 characters, 60 checks a minute each
(counted in memory), maps a studyset's language to LanguageTool's code
and keeps nothing. instance.info() names the languages it checks; the
privacy page says what goes there. sidecars/README.md has the steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:10 +01:00
3ef53d0481
docs(adr): ADR 0021 built, D55 and D56 amended to what was built
A draft is changed in a sheet of its own, as the composer saves a card
as soon as it is left; a note's cards come from the whole note, its
selection left for later; the entry is "Flashcards from a text". The
developer agreed to all three on 26 Oct.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:10 +01:00
e25c867616
feat(learn): the image an assistant asks to see shows beside the question
A card's image the model asks for (ADR 0021, D12) shows as a thumbnail in
the approval box, so the person sees what they approve; a PDF's page is
named by its number and title. The proposals and conversation sheets read
their cubits only in callbacks, as leancode_lint asks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:10 +01:00
6e63312bfa
docs: the assistant answering in Learn, as step 3 built it
Learn's AGENTS.md describes the field with the assistants answering in it
(ADR 0021, step 3): who answers, the scope, the conversation sheet and its
lookups, images, private studysets' warnings, kept conversations and the
tasks; and the testing trap of awaiting a finished subscription's cancel.
The shared packages' READMEs name providerTermsOf and onThisMachine.
Learn's README no longer says there is no end-to-end encryption, which
private studysets changed (ADR 0019).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:09 +01:00
2b7d04fd70
feat(learn): an assistant reads PDFs' pages, and sees images the person chooses or approves
ADR 0021, D12: text first. Lookups find a studyset's PDFs and read a page's
text as PDFium extracts it. An assistant that reads images is offered a
card's images as chips, and may ask to see a card's image or a PDF's page
drawn whole; the conversation asks the person each time (Show it, Don't
show) and sends nothing meanwhile. Images go only as png, jpeg, gif or webp
of at most 5 MB, are never kept in a turn, and a follow-up goes without them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:09 +01:00
cca10e855e
feat(learn): an assistant proposes flashcards from a text, a note or a PDF page
The cards come as drafts (ADR 0021, D10, D55): one question with one
tool, propose_cards, the cards in a sheet, every one chosen at first,
each changed in a small sheet of its own or left out, and only those
chosen added, marked as an assistant's (origin "assistant"). They are
proposed from a text pasted in Add material, from a note's title and
text, and from a PDF's page or the text selected on it, tied to that page
(D56). A note or a page of a private studyset goes only once agreed to
(D9), the text pasted counting as the person's own; nothing is offered
without an assistant. PDFs give their pages' text (PdfDocumentView.pageText).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:09 +01:00
8ba758c020
feat(learn): an assistant explains a card missed and checks what was written
A conversation has a task (ADR 0021, D14): asking, explaining a card or
checking what was written, which the system prompt says and the history
shows. Rating a card Bad offers Explain in a snack bar lifted above the
ratings (D53); a reading typed wrong asks Why? (D54), and the field's
Check has the text checked in the studyset's language, the answer in the
app's (D54). Explaining sends the card's own parts, in a conversation
named by its front.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:09 +01:00
bf46f8c460
feat(learn): a private studyset goes to an assistant only once agreed
ADR 0021, D9 and D17. Before a question sends a private studyset's
content to a remote model, through the parts chosen or a wider reach
with private studysets switched on, a warning names the studysets, the
provider's host and its terms (where checked: Anthropic, OpenAI,
Google, OpenRouter), and is asked once per studyset and assistant: the
agreement is kept in the person's sealed settings (schema 13,
privateAgreements, folded only where both rows hold it), shown in the
studyset's privacy dialog with Take back, and marked on the folded
button while it holds. Follow-ups and Try again ask again once taken
back, as the scope keeps where the parts came from (partsFrom). No
warning for an own assistant on this machine (loopback or localhost,
halkyon_llm's LlmEndpoint.onThisMachine); the local network may be
shared.

Texts that step 3 made untrue: the consent sheet now says what an
assistant looks up goes too and that conversations are kept encrypted
for the person's devices; the privacy dialogs no longer mention
showing a studyset to Claude; the privacy page says what is looked up
and when a private studyset goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:09 +01:00
114b75cdab
docs(adr): how step 3's last features are offered, D53–D56
Explaining a miss in a snack bar after Bad, checking as a wrong typed
reading's Why? and Check in the field, cards from text as drafts in a
sheet, made from a note, a PDF page or pasted text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:08 +01:00
806ffae202
feat(learn): the assistant answers in the field
The field asks an assistant the instance offers as well as searching the
web (ADR 0021): who answers is picked at its start, the one asked last
first (askWith, a device setting), an assistant not agreed to yet asking
for agreement first. Asking an assistant, every part of the topic may go
along, and one chip leads them with how far it may look (D49), only what
is chosen until the person widens it to the studyset, its tree or the
library, private studysets only where switched on (D50, D8). The
conversation shows in a sheet over the screen (D47), half the height and
dragged up to the full: each question with what went along, the lookups,
the answer as Markdown once whole, follow-ups, Stop, and Try again for a
question that got no answer; a device that can't seal says it keeps
nothing (D52). The field lists the conversations kept (D48), those about
where the person is first, to open again or delete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:08 +01:00
a125215942
feat(learn): asking an assistant in a conversation
ConversationCubit asks the assistant chosen each question with the
conversation so far (ADR 0021, D47), answers the model's lookups on the
device within the question's scope, eight rounds at most, the ninth told
to answer with what it found and the tenth ending the turn, every call
given its result so the conversation can still be sent; and keeps each
turn, sealed, once it is answered (D51): nothing of a question stopped
or failed, which can be asked again, and nothing at all on a device that
can't seal, which still answers (D52). The question and the parts sent
along are texts of their own, so a kept turn shows as it was asked, with
its lookups and its answer (TurnView).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:08 +01:00
df89ff9998
feat(learn): what an assistant may look up, and what it is told
The lookups a model may make in a conversation (ADR 0021, D7): the
studysets within reach, cards and notes found by what they say, a note
read whole; answered on the device, only within the scope the person
chose (D49, D50), private studysets only where switched on (D8), never
what is locked, never progress (D11). The question goes with each part
ticked, labelled, and the system prompt says how far the model may look
and that what it looks up is content, never instructions (D10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:08 +01:00
b965e05fdb
feat(learn): conversations with assistants, kept sealed (ADR 0021, step 3)
The first part of step 3, decided as D47 to D52 in ADR 0021: where a
conversation shows (a sheet over the screen), where kept ones are found
(the field), the scope's chip and that it starts with only what is
ticked, how conversations are kept, and what a device that can't seal
does.

Two synced tables: assistant_conversation (its title, assistant and
scope) and assistant_turn (one question and everything up to its answer,
never changed once written, ADR 0001, rule 2), so two devices continuing
a conversation offline lose nothing. SyncConversationGateway seals every
row with the person's key, a turn bound to its conversation, and reads
no row that isn't sealed: a device that can't seal keeps nothing (D52,
ConversationNotKept). Deleting one blanks it and its turns first (ADR
0002, rule 2). A turn keeps every message a follow-up sends back (tool
calls and results, the providers' own blocks), but no image, only that
one was sent.

The instance's privacy page says conversations are kept, encrypted, and
what the instance sees of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:08 +01:00
36899cdc3b
feat(app,learn,todo): the assistants in Settings and on the operator's screen
ADR 0021, step 2's app half. halkyon_app asks the module's assistant
endpoints (ServerpodAssistantGateway, ServerpodAssistantOperatorGateway)
and a person's own provider directly (halkyon_llm), behind
AssistantRepository: an AssistantChoice is a GrantedAssistant, asked
through the instance, which counts it, or a PersonalAssistant with the
person's own key, which the instance never sees. assistant_messages.dart
turns a conversation into the endpoint's models and back.

AssistantsCard, for an app's settings: what is granted and left of its
limits, agreed to once on a sheet saying what goes where, the person's
use by UTC month, the instance's privacy page, and their own assistants
where the operator allows own keys (kept ones stay listed, unasked,
where it doesn't). AssistantGrantsCard on InstanceOperatorScreen: grants
by handle with their limits in the pricing's unit, changed and taken
back after asking, everyone's use by month; a found account's menu
grants too. Nothing shows where nothing is offered or can be read.
package:halkyon_app/assistant_models.dart holds the models in pure Dart.

Learn keeps own assistants in the person's settings (ownAssistants,
schema 12, SettingsOwnAssistants); SettingsRepository.update refuses
(OwnAssistantRefused) to write a key where the settings would not be
sealed (SettingsGateway.seals). Learn depends on halkyon_llm directly.
Todo wires the gateways; a member offers no assistant. No feature asks
an assistant yet (step 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:07 +01:00
1e5d7040fd
docs(llm): the library doc names the Responses API
It still named two protocols after D46 added the third.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:07 +01:00
2da6555082
fix(base): the German privacy page names no "Betreiber"
Halkyon's German texts say "Admin" for the role and "wer die Instanz
betreibt" for whoever runs it, as the app's do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:07 +01:00
82e22cbc9a
build(deploy): assistants.env for the operator's assistants, Caddy forwards /privacy (ADR 0021, step 2)
- deploy/assistants.env.example: every variable commented, with a
  Claude example and a second assistant on the Responses API. The file
  itself is git-ignored and reaches Learn's server alone
  (compose.yaml's env_file, required: false, so an instance without it
  offers no assistant; that syntax needs Docker Compose v2.24 or later).
- deploy/.env.example points to it.
- The Caddyfile forwards /privacy to the web server, as it does
  /simplelogin-callback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:07 +01:00
dbae802915
feat(base,learn,todo): the operator's assistants, and the instance's privacy page (ADR 0021, step 2)
The server half of the assistants (points 4 to 8, D27 to D46): the
operator sets language models in deploy/assistants.env, grants them to
people with limits, and the module forwards their questions with the
operator's key, counting what each used, never what was asked.

- assistantsFrom: ASSISTANTS, ASSISTANT_ID_KEY, ASSISTANT_<ID>_NAME,
  _PROTOCOL (anthropic, openai, openai-responses), _BASE_URL, _MODEL,
  _KEY, _IMAGES, _PRICE_INPUT and _PRICE_OUTPUT (per million tokens),
  ASSISTANT_CURRENCY, ASSISTANT_OWN_KEYS; anything unusable stops the
  server as it starts, naming the variable.
- The assistant endpoint (identified): offered, agree (D40), ask as a
  Serverpod stream (D35): the text as the model writes it, then the
  whole turn, sent once the request is counted; figures, the caller's
  own. Checked in order: at most 2 questions on their way per person
  (tooManyAtOnce), the grant, the agreement, limits in the pricing's
  unit now (limitOutdated), the month's and the day's limits (with when
  they reset, UTC), the question (malformed, imagesNotRead, tooLarge
  past 400,000 characters, an image over 3,932,160 bytes or more than
  20 images); output capped at 16,384 tokens. Each request carries the
  person's opaque ID (HMAC-SHA256 under ASSISTANT_ID_KEY, D30). Once
  sent, the provider's reply is read to its end even if the app
  cancels, and counted on a session of its own; a provider's failure is
  logged by kind and status, told as providerKey, providerBusy (with
  its wait), providerRefused or providerFailed.
- The assistantOperator endpoint (identified and operator, on the
  register): setUp, grants, grant (assistantNotSet, invalidLimit,
  notFound), revoke, figures of a UTC month.
- Tables assistant_grant and assistant_figures (per person, assistant
  and UTC day), in the module's, Learn's and Todo's migrations. The
  daily pruning keeps this month and the two before
  (forgetOldAssistantFigures, D41); deletePerson deletes both.
  A member offers no assistant (D43).
- instance.basics() gains assistantOwnKeys.
- /privacy (D39, addPrivacyPageRoute, on the register's web server,
  before the profile pages' route): made from the instance's settings,
  English or German, no JavaScript: who may sign up, where the data and
  backups are, what the module keeps (the account, sessions, IP
  addresses, the server's log), the host's privacyPagePoints (Learn's:
  studysets synced, private ones encrypted on the devices with what
  stays readable, what is published, dictionary lookups, the learner
  count), each assistant with its model and host, what goes and what is
  counted, and whether personal keys are allowed. Never a key.

Tests: 70 on the assistants (grants, limits, refusals in order,
streaming, cancelling, counting, concurrency, the operator's denials,
deletion, pruning), 19 on the settings, 8 on the privacy page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:06 +01:00
038aca5439
feat(llm): halkyon_llm, language models' APIs streamed over plain HTTP (ADR 0021, step 2)
A pure Dart package, no SDKs, one interface (LlmClient) over Anthropic's
Messages API, OpenAI's chat completions, in each known host's dialect and
the local servers' (D45), and OpenAI's Responses API, stateless (D46). A
model's turn keeps what its provider wants back with the tools' results:
Anthropic's thinking blocks, the Responses API's reasoning items and
message phase, Gemini's thought signatures. Failures come by kind, the
provider's words cut and without keys.

Tested on replies written from the providers' documentation, replayed in
pieces of 1, 7 and 4,096 bytes; tool/record_llm.dart records real ones
with a key of one's own, which the tests then run on too (D36).
FakeLlmClient for the code that uses it. BSD-3-Clause, as halkyon_oidc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:06 +01:00
e4c7259df3
docs(adr): ADR 0021, chat completions' dialects and the Responses API (D45, D46)
D45: chat completions in each known host's dialect (OpenAI, Mistral,
Gemini, OpenRouter), in a common one to every other host. D46: OpenAI's
Responses API as a third protocol, stateless, its encrypted reasoning
sent back. D30's field at OpenAI is safety_identifier, at OpenRouter user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:06 +01:00
115683e180
docs(adr): ADR 0021, how the assistants are built (D35–D44)
Step 2's decisions: the answer streams over Serverpod's own stream; the
adapters are tested on replies written from the providers' documentation and
on recorded ones later; the assistants, their keys and prices sit in
deploy/assistants.env, passed whole to the server alone; the instance's
/privacy describes the whole instance; the agreement is kept on the grant;
figures are kept 3 months, by UTC's days and months; a member offers no
assistant yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:06 +01:00
31b9c17850
feat(app,learn,todo): the operator's "This instance" card, screen and badge (ADR 0021, step 1)
Whoever operates the instance approves, refuses and suspends accounts
and names other operators in the app (D21 to D24, D34):

- halkyon_app: InstanceOperatorRepository on
  ServerpodInstanceOperatorGateway (the module's instanceOperator
  endpoint; refusals as OperatorFailed: notFound, isOperator,
  lastOperator, notAllowed for 401 and 403, failed), in AppDependencies.
  OperatingCubit counts the accounts that wait: asked as the person's
  app opens, after every change, and every 5 minutes only while they
  operate the instance, so nobody else's app keeps asking.
  InstanceOperatorCard (or its tiles) and WaitingAccountsBadge show
  nothing at all for anyone else. InstanceOperatorScreen, laid out as
  Settings: the accounts that wait, each showing its four words to read
  out before Approve (D33), Refuse for new ones after asking; who
  operates, Take back after asking, the last always kept; Name an
  operator by handle; Find an account by the start of its handle, with
  Suspend after asking and Name as operator. English and German texts
  ("Admin" as the role word in German).
- Learn: the card right after the Sync card in Settings, which the
  cogs' count leads to as their dot leads to Sync; the count on the
  Settings cogs of Home and Profile.
- Todo: the tiles in the account sheet and the count on its button.
  Todo's server is a member, whose `operating()` is always null, so
  nothing shows there until it holds the register.

Tests: the gateway's mapping, the repository, both cubits with their
denials, the card, the badge and the screen; Learn's Settings shows the
card and the count to an operator and neither to anyone else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:05 +01:00
7ac715cd97
feat(app): a wait screen for accounts that wait for the operator (ADR 0021, step 1)
An account signed in or up to on an instance whose operator approves
new accounts, or one suspended, gets no session but a wait: the device
keeps its pass, words and handle in the keyring per instance
(SecureAccountWaitStore, InstanceStorage.waitOf), the auth status is
Waiting, and SessionGate shows WaitScreen in its own app, so the start
screen's sheets go with it. Every sign-in flow hands over to it: the
password's sign-in, sign-up, reset and account reset, SimpleLogin's
sign-in, sign-up and mending; their cubits show no problem then.

The wait screen shows the handle, the four words the operator reads out
for the person to match, and that Halkyon never asks anyone to read them
out (D33); it asks where the wait stands as it opens, every 30 seconds,
as the app comes back and when asked. A new account can be deleted with
its pass, a suspended one can't (ADR 0017). Approved, it leads to the
usual sign-in, once more as before (D31); a pass the instance knows no
more ends the wait. The start screen says where new accounts wait for
approval, and offers no Create an account where the operator takes none;
both refusals (closed, too many waiting) have their words, en and de.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:05 +01:00
a060fbc21d
feat(base)!: sign-ups as the operator says, waiting accounts, operators (ADR 0021, step 1)
SIGN_UP takes open (unset), approval or closed; anything else stops the
server as it starts. Closed refuses a SimpleLogin sign-up before its
handle is counted, an address sign-up only once the code has proved the
address, so nothing tells whether an address has an account. Approval
lets a new account wait, at most ten at once: no session is issued for a
waiting or suspended account, only a pass (32 random bytes, kept as its
SHA-256, 30 days, five per account) and four BIP-39 words the operator
reads out to match it. The pass looks, forgets, and deletes an account
that was never approved; nothing else.

Operators hold the scope operator, given first by SQL (D32), then named
in the app: InstanceOperatorEndpoint lists, approves and refuses waiting
accounts, finds accounts, suspends them (ending their sessions), and
names and takes back operators, never the last. account.operating()
counts the waiting for the badge. Members ask the register (five new
operations, a refusal kind signUp), and say closed while it is out of
reach. Never-approved accounts go after 14 days in the daily pruning.

The migration only adds the tables waiting_account and waiting_pass, so
the server before it keeps working on the new schema.

BREAKING CHANGE: a register and its members must run this version
together: a member asks the register operations an older one doesn't
know.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:05 +01:00
e2b3344e99
docs(adr): ADR 0021, D31: once approved, the person signs in once more
The wait screen says the account is approved and leads to the usual
sign-in, which opens the keys and sets up the authenticator: a pass is
never worth a session. The pass deletes only a new account that was never
approved; a suspended one keeps its data, and deleting it takes its
owner's password and a code, as ADR 0017 has every deletion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:05 +01:00
b2aa7667c4
refactor(core,keys): BIP-39's English list moves into halkyon_core
The list a recovery phrase's words are of moves from halkyon_keys into
halkyon_core, exported as bip39English, so that the server module can
take a waiting account's matching code from it (Halkyon Learn's ADR
0021, D33) without depending on halkyon_keys and libsodium. Its test
(2048 words, sorted, the published file's SHA-256, unique by their first
four letters) moves along; its MIT licence too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:05 +01:00