Repository navigation
feat: Insights — pick meetings, ask a question, keep the answer [D-724] - #261
Merged
Merged
Conversation
…Operator [D-715, D-716, D-723]
Drop 1 of the insights branch. Three units, each of which leaves the branch
runnable on its own.
D-715 — `cassini.meetings.context.v1` moves to its own package,
`internal/meetingcontext`, exporting the types, `Build`, `RenderMarkdown`,
`EncodeJSON`/`DecodeJSON` and `RenderOpts{Timestamps}`. It depends on the
standard library alone, so a consumer can render a bundle without the CLI,
a catalog type or ffprobe. `cassini meetings context A B C` now takes N ids
and prints one document in the order you named them; an id you cannot read
fails the whole run rather than silently shrinking the set. `--timestamps`
adds MM:SS citations. Golden fixtures pin the single-meeting bytes, taken
before the move, so byte-identity is proven rather than asserted, and
`spec/cassini-meetings-context-v1.schema.json` publishes the shape.
D-716 — catalog entries carry `hasSummary` and `wordCount`, so the browse
list and the Prepare panel can state what a selection contains without
downloading every `.opus`. Both optional and absent-means-unknown by
construction: pointers in Go, optional properties in TypeScript, so an
archive published before this ships reads as unknown rather than as "no
summary" or "no words".
D-723 — two top-level surfaces, Browse and Operator, with configuration
inside Operator behind a left nav (Console: Recordings; Settings: AI
providers, Publish pipeline, Insight templates), as the v3 prototype draws
it. Replaces the third `settings` surface. A new `panel` hash param names
a panel inside a surface, which is what a deep link from an "unconfigured"
notice will need.
Two bugs fell out and are fixed here. Shell hash params were appended past
`t=`, which silently killed a meeting's seek time because `parseTimeHash`
is anchored at end-of-string; `job=` had it already on main. And the run
console's master/detail split was decided by a viewport media query that
cannot see the new nav rail, so it now measures the console's own width.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQcx96S8fnmqVz8fJ3dXAn
… it [D-717, D-626, D-656] Drops 2 and 3 of the insights branch: the demo path end to end, minus the in-app Generate button. D-717 — `GET published/meetings-context?id=…&id=…` serves the same document `cassini meetings context` prints for the same ids, proven by a golden test that builds the real CLI, packs a real `.opus` and compares the bytes both ways. The route rides the already-declared `^published\/.+$` at USER, so there is no manifest change. Access control is the existing per-caller catalog filter, reused rather than reimplemented; an id the caller may not read is a 404, indistinguishable from one that does not exist. Recordings are fetched as the caller, staged in a temp directory the request always cleans up, and handed to `cassini` under the request's context so a cancelled request leaves no subprocess. Capped at 20 meetings a request, because each id costs a whole recording download. `cassini meetings context --local <paths…>` reads portable `.opus` files instead of Nextcloud, which is how the operator produces a bundle for a caller whose app password it does not have. `--catalog` supplies the room names an id run would have read from the catalog, so the two documents match by construction. D-626 — pick meetings across rooms in the browse list and Prepare a bundle. A pure `selectionModel.ts` holds the picks in the order they print, reconciles them against every catalog the shell observes so a meeting that leaves the archive is said out loud rather than silently dropped, and owns the Prepare panel's arithmetic. The gap disclosure reads the catalog alone and keeps three states apart: has no summary, does not record whether it has one, and has one. The bundle arrives through the DataProvider seam; `StaticCatalogProvider` deliberately does not implement it, so a standalone export — which has no operator behind it — offers no Prepare rather than assembling a lookalike in the browser. D-656 — `internal/insight` is the Run seam: provider, workflow, context, artifact. It consumes the exported context bundle, makes exactly one completion call through an injected provider, and returns the body plus a record of what actually produced it. `transcribe.ChatCompletion` is exported so something other than the pipeline can reach the endpoint, with a typed `*APIError` so "the provider refused" and "the model failed" are distinguishable. `cassini insight run` writes the document with its provenance as frontmatter and, under `--record`, the same record as JSON. Exit status says what happened, deliberately not the pipeline's warn-and-skip: a document you asked for and did not get is not a warning. The workflow registry holds one entry — the shipped summary prompt, hashed from its own bytes — because "registry of one, hashed" and "registry of five, hashed" are the same struct. Populating it is D-718. One cross-unit bug fixed: `ExAppConfig.CassiniBin` was read from the raw environment before `--cassini-bin` had been resolved, so on a flag-configured deployment the endpoint was never mounted and Prepare failed looking like a Nextcloud permissions problem. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQcx96S8fnmqVz8fJ3dXAn
…adiness bit [D-718, D-719, D-722]
Drop 4 of the insights branch.
D-718 — the workflow registry drop 3 built is now populated and visible.
Every prompt this product can send lives in one place,
internal/insight/workflows/prompts/, and the publish pipeline's summary
step reads its bytes from there rather than embedding its own: there is
exactly one copy of every prompt in the tree, and the pipeline literally
sends the registry's `summarise` workflow. The prompts moved rather than
being copied because internal/transcribe links the cgo speech recogniser,
and the drift gate has to run in lint.yml — the only workflow a
prompt-only pull request triggers.
Two workflows ship, `summarise` and `todos`. The prototype's other three
have no prompt authored anywhere and are absent rather than invented; a
workflow whose bytes do not resolve is simply not in the registry.
`cassini insight workflows --json` prints the set including the exact
instruction each one sends, `GET operator/settings/workflows` serves it
read-only over the already-widened settings route, and the Insight
templates panel renders it — loading, empty and failed being three
visibly different answers.
D-719 — LLMSettings carries an Insight step beside Summary, so a
deployment can summarise on a small local model and answer a question on
a larger hosted one. An insight step with no endpoint of its own is not
"insights off": it inherits the summary endpoint and the panel says so,
because the two behave differently the moment that endpoint is repointed.
Per-step TIMEOUT_SEC and MAX_TOKENS replace the shared pair — one
variable cannot carry two values once there are two endpoints, and the
whole reason the bounds live on the endpoint is that a CPU-bound local
model needs a longer leash. LLMStep.Template is added now rather than
migrated in later. A settings file written before this change loads,
round-trips losslessly and reads as "insight inherits the summary
endpoint". Keys stay write-only.
D-722 — GET operator/setup, the one USER-level operator route, carries
`features: {summaries, insights}`. One bit each, no URL, no model, no
key, and no manifest change — a field on a declared route costs nothing
at the proxy. It drives a single component whose copy lives in tested
TypeScript, which gives an administrator a route-preserving deep link to
the panel that fixes the problem and gives everyone else a statement of
fact rather than a control that would 403.
Three things only visible where the units meet, fixed here.
`features.insights` meant "a provider row exists", which is false about
the code: ChildEnv emits nothing for a step that resolves to no provider,
so a provider nothing points at reaches the recorder as no endpoint at
all — the app would have promised insights that answer only "no model
endpoint is configured". It now reads the resolved insight step.
`cassini insight run` was still reading SUMMARY_*, so the entire INSIGHT_*
wire was consumed by nobody. And both Workflow fields are now checked
against the registry the same drop published — advisory, never a gate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQcx96S8fnmqVz8fJ3dXAn
…n the catalogue [D-700, D-720, D-721] Checkpoint. All five units of drop 5/6 are built and each has been reviewed, but the integration, adversarial-verify and fix passes did NOT run — the session was stopped first. Everything below builds and every suite passes (operator and recorder green, viewer 542, app 209, gofmt clean), so this is a safe place to resume from, not a broken tree. It is NOT reviewed-and-fixed to the standard of the four commits before it. D-720 — insight_runs + insight_run_attempts as migration 0008, beside job_attempts. Status queued|running|succeeded|failed, retry as attempt_number+1 on a stable id, and the status as the lock. D-700 (server) — the run path: resolve the caller, resolve which meetings they may read through the existing per-caller filter, create the record and answer 201 immediately, then run in the background and PUT the document into the requester's own Nextcloud home. D-700 (manifest) — the three insight routes declared as one block, the app's first mutating USER routes. And docs/exapp-update-constraints.md §5a, the section the planning corpus has been citing for weeks and which did not exist: an in-place app_api:app:update DOES refresh the route allowlist, but only when the manifest carries a new <version>. Measured against a real Nextcloud 34 + AppAPI by reading AppAPI's own oc_ex_apps_routes rows, with a withheld route as the negative control and a re-register as the positive one. harness/bin/check-route-refresh.sh is that check, committed so the answer is reproducible rather than a claim. D-700 (app) — the Generate card in the Prepare panel's reserved slot, with the honest progress the prototype does not model: queued, running, then a result or an error you can act on and retry. D-721 — insights as cards in the browse catalogue behind a type filter, the insight document in the same sheet, and links both ways between a meeting and the insights that used it. An insight surfaces under every room it drew from, which is the one place the prototype is provably wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQcx96S8fnmqVz8fJ3dXAn
The integration, adversarial-verify and fix passes 91389fb was checkpointed without. Six confirmed findings and eleven low-severity ones, each with a test that fails without the fix. The cross-unit bug: the Generate card offered "or ask your own question" as an unconditional control, and the operator refuses every use of it. A question is only accepted by a workflow whose prompt carries a {{QUESTION}} slot, and neither shipped workflow has one — so the headline gesture of a feature described as "ask one question of all of them" answered 400 with no way to discover why beforehand. Every unit was correct alone: the app built against the contract, which has a question field; the server implemented the refusal; the registry deliberately shipped no question-taking prompt. The card now reads the same prompt bytes the operator decides on, so the box appears only for a workflow that can take one and returns on its own the day such a prompt ships. `cassini insight run --question` also did not exist, though the operator was already passing it. Also fixed: a failed per-caller catalog scan was reported to the user as "one of these meetings is not available to you" — a permission claim about an outage, and written permanently onto the run. Both call sites now answer 502. A retry against a run that had already succeeded said "this insight is already running". The document read enforced its 4 MiB cap only after reading up to 64 MiB. Meetings were grouped by a UTC clock and insights by the reader's, so west of UTC a month boundary emitted its groups out of order. The "No insights yet" empty state contradicted the result line above it. And the selection bar stopped counting picked meetings when the type filter hid them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQcx96S8fnmqVz8fJ3dXAn
…D-717] AppAPI's proxy is PHP, and PHP collapses repeated query parameters to the last value unless they carry a `[]` suffix. So the app's `?id=a&id=b&id=c` reached the operator as a single id, and Prepare answered a three-meeting request with a one-meeting document and a 200 — a correct-looking answer to a question nobody asked. Measured against a real Nextcloud: three ids sent, one id in the operator's log. Nothing on this branch exercises that hop. The unit tests, the integration and verify passes, and the golden test that compares endpoint bytes against `cassini meetings context` all call the handler directly, so all of them passed with this present. It surfaced the first time the feature ran in the topology it ships in. Ids now travel as one comma-separated `ids`. A repeated `id` is still accepted, because a caller reaching the operator directly — the CLI, the tests, a standalone deploy — is not behind that proxy, and the CLI-parity golden test compares against `cassini meetings context A B C`. Giving both forms is a bad request rather than a silent precedence rule. `POST insights` is unaffected: its meeting ids travel in a JSON body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQcx96S8fnmqVz8fJ3dXAn
delasous
marked this pull request as ready for review
September 8, 2026 22:01
Review pass over the Insights branch, against Alex's v3 prototype.
Fixed: registering an AI provider is now the whole of the setup an
insight needs. An insight only resolved an endpoint through a pipeline
step somebody had also switched on, so a saved endpoint still read as
"No AI endpoint is available" everywhere. insightEndpoint resolves the
insight step, then the summary step, then any registered provider, and
the operator hands that resolved endpoint to the run — so the settings
page and the child's environment cannot disagree.
Fixed: an insight document no longer opens with a paragraph of YAML. The
recorder writes provenance as front matter and markdown renders `---` as
a rule, so twenty quoted keys arrived above the answer. The panel shows
the same facts in its header and its "This run" list.
Added: "Ask your own question", the freeform template. The machinery
existed ({{QUESTION}}, TakesQuestion); the prompt carrying the
placeholder did not. Offered wherever a question can be typed and
withheld where one cannot — not as the publish pipeline's summary step,
which runs unattended with nobody to ask.
Added: the asker picks the endpoint and model when creating an insight.
Migration 0009 stores what was ASKED FOR beside what answered, which
reverses D-720 §4 deliberately — §4 was written when nobody could
choose, so re-resolving could only help. A retry re-runs the request.
`operator/ai/providers` is USER, carrying ids and display names only:
which endpoint sees your transcripts is the asker's decision, so the
list has to reach them. THE RELEASE THAT SHIPS THIS MUST BUMP <version>,
or AppAPI never rewrites the route table.
Added: registering the first endpoint switches summarising on, pointed
at it. Once only, on the save that takes a deployment from no endpoint
to one, so turning it off stays off.
Changed: AI providers configures endpoints and nothing else, and reports
whether each one's model list actually came back. Publish pipeline is
the whole pipeline in order, with summarisation moved in from AI
providers and given a card of its own. The summary step names a template
from the registry instead of a free-text workflow id, and its model is a
combobox that loads on focus. The locked state is one card everywhere.
Meetings/Insights moved into the rooms rail. A meeting's header carries
its identity and stays put while the transcript scrolls under it, with
the insights it was used in under the summary rather than below the
whole transcript.
Removed: the Insights step in AI providers, which configured "run
insights somewhere else" while looking like the switch that turns them
on. The Prepare panel's list of previous runs, which was a second place
insights were listed. The "does not record whether it has a summary"
gap sentence, true of every meeting published before the field existed.
Conflicts, and how each was resolved: - run_test.go — main replaced the hardcoded migration-version list with assertAllMigrationsApplied, which does not need updating when a migration is added. Took main's; it makes this branch's migrations pass with no edit at all. - SettingsPanel.svelte — main added the participant/project vocabulary to the same panel this branch rebuilt. Kept both: the terms ride the STT PUT beside quality and the device override, so they belong to sttDirty, and a page whose only edit was a term still sends that request and not the LLM one. Git had also mis-aligned main's handleSave body into this branch's loadLLM; the terms are on the save. - transcribe.go — this branch factored the shared LLM config into sharedLLMConfig(); main still had it inlined. Kept the helper and main's line saying summarisation is the only LLM step left. - transcription-pipeline.md — kept main's decoder-hints row and this branch's corrected prompt paths after the registry move (D-718). - privacy.md, configuration.md, architecture.md, llm_settings.go — main describes the world before insights (one LLM step); this branch is what adds the second. Took this branch's, which is a superset. Migration numbering collided in a way git could not see: both sides added an 0008. main's 0008_job_room_columns is released and keeps the number, so this branch's move up — 0009_insight_runs and 0010_insight_requested_endpoint. Nothing has shipped either of them, so only development databases are affected.
The remote branch had its own merge of main plus a #270 recovery. llm_settings.go is the one resolution worth reading. Both sides had independently fixed the same class of bug — an insight falling past a usable endpoint — at different widths: remote summarising switched OFF emits no SUMMARY_* for the recorder's own layering to inherit, so it materialised the summary step's selected provider as INSIGHT_* instead. here insightEndpoint resolves the insight step, then the summary step, then any registered provider, and ChildEnv emits from it. Neither subsumed the other: this branch consulted the summary step through provider(), which returns nothing for a step that is off, so it fell past exactly the endpoint the remote branch had rescued. The merged insightEndpoint takes the summary step's provider by ID, whether or not that step is switched on — switching summarising off means "publish meetings without a summary", never "stop answering a question somebody asked by hand" — and keeps the third fallback for a bare provider with no step at all. The remote branch's test for its case survives and passes unchanged against it. LLMSettingsPanel.svelte took this side: the remote still carries the Insights step, the free-text Workflow field and the model datalist that this review pass removed.
This was referenced Sep 8, 2026
Merged
Member
|
I would say LGTM and ready to merge. Some details I've found:
|
ikusteu
force-pushed
the
feat/insights
branch
from
September 9, 2026 18:26
8fe3a98 to
9397a01
Compare
…ked them PR #261 carried ~30 MB nobody meant to commit: a 14 MB Linux `cassini` binary built by hand into the module's own bin/, a 7 MB webm and a 7 MB opus from `cassini` runs four months older than the branch, and a stray personal scratch directory. All of it was untracked working-tree output that a broad `git add` during a conflict-resolution merge swept in. It has been removed from that branch's history; this is the part that stops it happening again. .gitignore names the two paths we now know about. It cannot be the whole answer — an ignore file only covers directory names somebody already thought of, and the next scratch directory will be called something else — so scripts/check-added-blobs.sh judges what a diff ADDS: no blob over 512 KiB, and no ELF, Mach-O or PE executable at any size, since a small binary is no more reviewable than a large one. Escape hatch is a path in .github/large-blobs-allow.txt, which lands in the diff and gets reviewed. 512 KiB is measured against the repo: the largest legitimate file anyone has added is a 361 KB screenshot, and every accident so far is more than ten times the limit. It deliberately does not catch the two ~250 KB transcript JSONs from the same incident — they are smaller than a screenshot the repo already ships, and no size threshold separates them from one. That is what .gitignore is for, and why both landed together. The gate runs in lint.yml, not ci.yml: ci.yml is path-filtered, and the whole point is a file whose path nobody anticipated. `harness/go-talk-rotator/three-song-rotator` is a 15 MB compiled binary already on main and is deliberately not allow-listed — the gate judges additions, so it stays green now, and asks the question the day anyone rebuilds it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKpp7xqJ7ewtrwRRJANfhQ
Member
|
Tried this manually, it works for and looks really good. It's not without gaps, but that's expected and accepted. Had the agent run a review and it came up with an insane number of things to fix (ofc). In the spirit of merging everything ASAP and fixing downstream, I had the agent open a new ticket for downstream fixes (inspection first), and am merging this in |
delasous
added a commit
that referenced
this pull request
Sep 14, 2026
fix: #261 follow-ups before 0.2.0 — model truncation, base-path routes, endpoint disclosure, retry races, docs [D-740]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pick several meetings, ask one question of all of them, keep the answer.
What it does
Browse — insights sit in the meeting list as cards, filtered by Meetings/Insights in the rooms rail. A meeting's header carries its name, room, date, length and speakers and stays put while the transcript scrolls under it; the insights it was used in read under its summary.
Prepare — pick meetings, review what the bundle contains, then create an insight: choose a template, see the question it will ask, choose the endpoint and model, Generate.
Operator settings — AI providers registers endpoints and nothing else. Publish pipeline is the whole pipeline in order: detected hardware and its device override, quality, summarisation. Insight templates lists what the image ships, with the exact prompt bytes and the hash a document records.
Decisions worth reviewing
Registering a provider is the whole of the setup an insight needs. An insight resolves the endpoint its own step names, then the summary step's — switched on or not — then any registered provider. So every endpoint you register is one an insight may reach, and removing them all is how insights are switched off.
docs/privacy.mdcarries this.Registering your first endpoint switches summarising on. Once only, on the save that takes a deployment from no endpoint to one. An install that has just configured an endpoint and still publishes meetings without summaries has done the work and not got the feature. Turning it off afterwards stays off.
The asker picks the endpoint, and a retry re-runs what they asked for. This reverses D-720 §4 ("a stored provider is a record, not an input"), deliberately: §4 was written when nobody could choose, so re-resolving could only ever help. Now it would silently answer a different question. Migration 0010 stores what was asked for beside what answered.
The provider list is USER, not ADMIN.
GET operator/ai/providerscarries ids and display names only — the base URL, the key and the request bounds stay administrator-only. Which of the configured endpoints sees your transcripts is the asker's decision, so the list has to reach them. The template picker stays admin-only, because its registry genuinely is ADMIN and would 403."Ask your own question" ships as a workflow. The machinery existed (
{{QUESTION}},TakesQuestion); the prompt carrying the placeholder did not. It is offered wherever a question can be typed and withheld where one cannot — not as the publish pipeline's summary step, which runs unattended with nobody to ask.Before release
Bump
<version>inappinfo/info.xml. This adds^operator\/ai(\/.*)?$, and AppAPI only rewrites an app's route table on a version change — measured on this branch, withharness/bin/check-route-refresh.shas the check. Without the bump the endpoint picker cannot list providers and every run silently falls back to the deployment's configured endpoint.Migration numbering.
0009_insight_runsand0010_insight_requested_endpoint. They were 0008/0009 until main landed0008_job_room_columns; nothing has shipped either, so only development databases are affected.Not yet exercised
Suites
Both Go modules build, vet and test clean;
gofmt -s -lsilent. cassini-viewer 607, cassini-app 239.🤖 Generated with Claude Code
https://claude.ai/code/session_01Tr66bBkgDvRCkzr2Zcq3Wp