Foundational entity for the AI Workbench Runner. A Mission carries the
user's standing instruction (concept + objective), references to the
modules it should draw context from, a cadence, and an append-only
history of iterations. Each iteration records the plan the AI generated
for that run plus the resulting proposal statuses and user feedback.
- `data/ai/missions/types.ts` — Mission, PlanStep, MissionIteration,
MissionCadence union (manual / interval / daily / weekly / cron)
- `data/ai/missions/cadence.ts` — pure `nextRunForCadence(cadence, from)`
used by the store on create / update / finishIteration
- `data/ai/missions/store.ts` — CRUD + lifecycle
(pause / resume / complete / archive / delete) + iteration helpers
(start / finish / addFeedback)
- `data/ai/module.config.ts` — new `ai` sync app; Missions sync
cross-device (unlike the local-only `pendingProposals`)
- `db.version(18)` adds the `aiMissions` Dexie store with indexes on
state, createdAt, nextRunAt, and [state+nextRunAt] for the Runner's
"due now" query
Iterations live inline on the Mission record — append-only, small N,
always loaded together by the Planner. No separate child table.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- COMPANION_BRAIN_ARCHITECTURE §20: Actor model, policy layer,
pendingProposals lifecycle, ghost-UI pilot, roadmap, open follow-ups,
manual test snippet
- DATA_LAYER_AUDIT §9: new Actor columns on records
(`__lastActor`, `__fieldActors`), `pendingProposals` table, write-path
diagrams for user / AI / approval, open mana-sync Go + Postgres work
- apps/mana/CLAUDE.md: short AI Workbench section with pointers + Dexie
hook now lists actor stamping
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
First pilot of the AI Workbench ghost-state pattern. A reusable
`<AiProposalInbox module="todo" />` component renders pending proposals
for a given module as dashed-outline ghost cards above the real content —
zero UI when the AI is idle, approve / reject inline when it's not.
- `data/ai/proposals/queries.ts` — reactive `useAiProposals` live query
with module / status / missionId filters. Module filter resolves via
the tool registry so each proposal auto-routes to the right page.
- `components/ai/AiProposalInbox.svelte` — the drop-in inbox component.
Shows tool description + params + AI rationale; approve runs the
original intent under the AI actor context (preserving attribution),
reject stores the row with status=rejected for the next planner pass.
- Wired into /todo for the pilot. Other modules opt in by adding one
line once their tools land in DEFAULT_AI_POLICY.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The prior dance (93bb94a12 drop .ts, bb278fb3c switch to .js) kept
breaking one consumer or the other:
- bare specifiers (no extension) satisfied svelte-check but broke the
Node ESM loader invoked via @tailwindcss/node during SSR — SSR of
every (app) route 500'd with ERR_MODULE_NOT_FOUND on 'theme'.
- .js extensions satisfied svelte-check and Vite but still broke the
Tailwind loader, because the files on disk are .ts — Node ESM walks
the actual filesystem and can't rewrite .js → .ts the way tsc does
at type-check time.
Flip the web app's tsconfig to "allowImportingTsExtensions": true and
put the .ts extensions back. tsc now accepts the imports, and Node's
loader finds the real file on disk. No build step, no emit, and the
shared-types package stays a pure source-only TS workspace.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds the staging layer that turns AI-attributed tool calls into user-reviewed
proposals instead of direct writes.
- `data/ai/policy.ts` — per-tool AiPolicy (`auto` | `propose` | `deny`) with
module-level defaults and a global fallback. `user` and `system` actors
always bypass (they ARE the decision / are trusted subsystems).
- `data/ai/proposals/` — Proposal + Intent types, store with
create/list/approve/reject/expire. Proposals are local-only (do NOT
sync); the approved write syncs through the normal module path.
- `tools/executor.ts` routes by actor+policy: `auto` runs directly under
`runAsAsync(actor, ...)`, `propose` stages a Proposal carrying rationale
+ mission metadata, `deny` refuses. `executeToolRaw` bypasses the policy
gate — used only on the approval path where consent already exists.
Default policy is conservative: read-only and append-only self-state
(log_drink, log_meal) auto-execute; everything that mutates user-visible
records defaults to propose.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extends the creating/updating hooks to capture the ambient actor
synchronously and freeze it onto every write:
- `__lastActor` on each record (whole-record attribution for Workbench badges)
- `__fieldActors` parallel to `__fieldTimestamps` (field-level attribution
for inline diff rendering — e.g. "AI changed due date, user changed title")
- `actor` on `_pendingChanges` rows so mana-sync + cross-device views can
distinguish AI- vs user-initiated writes
Also adds `kontextDoc` to v17 (missing from schema while module was live)
alongside the new `pendingProposals` table for staged AI intents.
Actor is captured in-hook rather than at emit time because
`trackPendingChange` is deferred via setTimeout and would otherwise lose
ambient context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a discriminated Actor union (user | ai | system) threaded through
the event pipeline so downstream consumers can distinguish human writes from
AI-initiated ones and derived subsystem writes.
- `EventMeta.actor: Actor` is required (no legacy fallback — pre-launch)
- `emitDomainEvent` takes an options bag `{ actor?, causedBy? }`; falls back
to the ambient actor set by `runAs` / `runAsAsync`
- `runAs` / `runAsAsync` pin the actor at defined boundaries (tool executor,
mission runner, projection dispatcher) — primitives capture synchronously
so ambient context is never SoT past the write moment
Foundation for the AI Workbench. Follow-up: mana-sync server must accept
and persist `actor` in pending-change payloads.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New Mana module "Kontext" — one editable markdown document per user,
displayed as a workbench page. Toggle between rendered preview and a
raw textarea editor (Cmd/Ctrl+E); debounced autosave 500 ms.
Content is encrypted at rest (new `kontextDoc` table, singleton row).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Commit 93bb94a12 dropped the extensions on shared-types re-exports
to make the web app's svelte-check pass (its tsconfig has no
allowImportingTsExtensions). That satisfied tsc but broke SSR: the
dev server tripped with ERR_MODULE_NOT_FOUND on every (app) route
because Node's native ESM loader (used by downstream tooling like
@tailwindcss/node) cannot resolve bare relative specifiers without
an extension, and only Vite-owned paths got the bundler-style
resolution the fix relied on.
Switch to the TypeScript-ESM idiomatic `.js` extension. tsc with
moduleResolution: "bundler" still type-checks against the actual
.ts source, and at runtime both Vite and Node resolve `.js` the
same way — no tsconfig flag flip required.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The .mana backup parser in src/lib/data/backup/format.ts imports
inflateRaw from pako but the package was never declared. The
production Vite build fails to resolve it — pnpm let it through
locally only because some other workspace dep hoists pako into
node_modules.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- mail/ListView: add a11y_click_events_have_key_events ignore to match
the existing a11y_no_static_element_interactions suppression
- sleep/MorningLog + companion/CompanionChat: mark intentional
initial-value state reads with state_referenced_locally ignore
- goals/GoalEditor: add tabindex="-1" to the dialog role element
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- shared-types/index.ts: drop .ts import extensions (web-app tsconfig
has no allowImportingTsExtensions; bundler resolution handles it)
- backup/format.test.ts: narrow buildZip return to Uint8Array<ArrayBuffer>
so the Blob() constructor accepts it under strict lib.dom
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Survey of cross-domain AI context systems (Gemini Personal Intelligence,
Apple Intelligence, Qira) and architectural options for a Mana-wide
reasoning layer over the 40+ modules.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Local-first module with meditatePresets/Sessions/Settings tables, hub
ListView with stats + recent sessions, and SessionPlayer with
BreathingCircle + MoodPicker. Route at /meditate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds four audit scripts (module health, inter-module coupling, per-function
cognitive complexity, D3 treemap) with generated reports under docs/ and
an iframe-embedded workbench app at /admin/complexity. Reports regenerate
weekly via the module-health GitHub Action.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
eventstream was confusingly branded "Events" in the app registry,
colliding with the real events calendar module. Renamed to activity
(DE: Aktivität) since it's a live activity feed across all modules.
cycles -> period (DE: Periode) makes the menstrual-tracking module
self-describing. Tables cycles/cycleDayLogs/cycleSymptoms renamed to
periods/periodDayLogs/periodSymptoms; field cycleId -> periodId;
TimeBlockType 'cycle' -> 'period'; domain event CycleDayLogged ->
PeriodDayLogged. Generic "cycle" usages (billing, lifecycle, breath,
bicycle, import cycles) left untouched.
Constant disambiguation: prior DEFAULT_PERIOD_LENGTH (bleeding days)
renamed to DEFAULT_BLEEDING_DAYS; prior DEFAULT_CYCLE_LENGTH (28d full
cycle) is now DEFAULT_PERIOD_LENGTH.
Pre-launch, no data migration needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moves the BYOK key CRUD from the standalone /settings/ai-keys subpage
directly under the new BYOK tier card in the main AI settings section.
Users now manage keys in-context where they toggle the tier.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Following the shared-icons fix (d5cabed14), audit every workspace
package's src/index.ts for top-level side effects and flag the
ones that are safe to tree-shake:
- Pure TS re-export barrels (types, theme, utils, llm, storage):
"sideEffects": false — lets Vite prune entire submodules when a
consumer only imports a subset of named exports. Matters most for
shared-llm where the orchestrator/BYOK branch isn't needed on
every route.
- Packages that ship .svelte components (branding, ui, links):
"sideEffects": ["**/*.svelte", "**/*.css"] — same tree-shaking
benefit for TS modules, but keeps Svelte component CSS injection
intact.
The state-holding submodules (shared-ui drag-state/toast,
shared-llm store, shared-links mutations) are still evaluated
whenever their exports are referenced, so behaviour is unchanged —
the flag only lets the bundler skip modules that aren't in the
dependency graph at all.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- DATA_LAYER_AUDIT.md: new section 8 covering the export/import flow
end-to-end — architecture diagram, .mana format, protocol-stability
commitments we locked in pre-launch (eventId + schemaVersion + op
vocab + tombstones-forever), encryption-boundary argument, file
map, and the remaining backup backlog (M4b, M5, signature,
resumable download, dedup table).
- services/mana-sync/CLAUDE.md: /backup/export row in API table with
explicit note that it sits outside the billing gate, new Backup /
Restore section with format sketch + split between writer.go (pure)
and handler.go (shim), test-coverage line mentions the backup cases,
project-structure tree lists backup/*.go, Security section mentions
RLS still applies to the export path.
No code changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refactor: HTTP handler becomes a thin shim over a pure WriteBackup(w,
userID, createdAt, iter) function. RowIterator abstracts the store, so
tests feed synthetic ChangeRow slices and production feeds
StreamAllUserChanges. Zero behavior change in production — same bytes
on the wire.
Tests (all pass):
- TestWriteBackup_Roundtrip: three rows across two apps, assert zip has
2 entries, events.jsonl has 3 JSON lines in order, insert omits
fieldTimestamps, update surfaces them, manifest apps are sorted,
eventsSha256 equals a recomputed sha of the decompressed body.
- TestWriteBackup_EmptyUser: empty userID refused up-front.
- TestWriteBackup_NoRows: zero-row export still produces a valid zip
with an empty events.jsonl and a manifest with eventCount=0 and a
non-empty sha (sha of empty input).
- TestWriteBackup_DefaultsSchemaVersionZeroRowsToOne: legacy rows with
schema_version=0 clamp to 1 so the manifest never claims a protocol
version that never existed.
Paired with the vitest zip parser suite on the TS side, this closes
the Go-writes / JS-reads round-trip without needing live mana-sync.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
phosphor-svelte ships a 28 MB lib of per-icon Svelte components and
does not declare "sideEffects" in its own package.json. When
@mana/shared-icons re-exports that package without its own
"sideEffects" hint, Vite/Rollup conservatively assume every
transitive module evaluation might matter and cannot aggressively
prune unused icons across chunk boundaries.
Our re-exports (index.ts: `export * from 'phosphor-svelte'` + a small
name→component registry) are pure ESM barrels with no top-level
runtime code, so flagging the package as side-effect-free is safe
and lets the bundler drop unused icons and skip evaluating the
icon-registry module from chunks that only want a named icon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Builds synthetic PKZIP archives in-memory (same deflateRaw the runtime
uses on the inflate side) and asserts:
- round-trip through parseBackup surfaces manifest + events + matching
sha256
- events.jsonl iteration yields both records with fieldTimestamps intact
- wrong formatVersion is rejected with a clear error
- missing manifest.json or events.jsonl is rejected by name
- non-zip input is rejected at EOCD scan
- sha mismatch surfaces as differing manifest vs computed hash fields
- iterateEvents skips blanks + throws on malformed JSON
This is the only untrusted-input frontier in the backup flow, so it
earns a real test harness rather than relying on integration smoke.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Client-side restore for the same-account case:
- lib/data/backup/format.ts: hand-rolled .mana (zip) parser. Walks the
central directory, inflates DEFLATE entries via pako (already in the
repo), exposes manifest + events.jsonl + recomputed sha256. No new
dependency; the archive shape is narrow enough that 200 lines cover it.
- lib/data/backup/import.ts: validates manifest (userId match is hard-
refused, eventsSha256 must match, schemaVersionMax ≤ client support),
streams events through iterateEvents(), batches 300 per appId and
replays via the existing applyServerChanges() path. LWW makes the
operation idempotent.
- settings/my-data: file picker, progress bar, per-phase labels, success
summary with event count + source timestamp.
Scope is intentionally same-account only: events originate from the
server for this user, so re-pushing them is unnecessary. Cross-account
migration needs the MK transfer path in M5.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bug: setting taskOverrides['companion.chat'] = 'byok' didn't work
when the user's allowedTiers was empty/['none']. The tier-too-low
check in run() compared task.minTier ('browser') against userMaxTier
('none') and threw TierTooLowError before the override was even read.
Same issue in canRun() and candidateTiers().
Fix: when a per-task override exists, treat it as opt-in to that tier
even if not in the global allowedTiers. The override is the user's
explicit per-task signal — overriding the global default is exactly
what an override is for.
- run(): effectiveMaxTier = max(override, userMaxTier)
- candidateTiers(task, override): adds override to baseTiers
- canRun(): now passes the override to candidateTiers
The Companion chat now correctly uses BYOK when selected from the
toolbar, even if the user hasn't enabled BYOK in their global LLM
settings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The single $effect that wired SceneAppBar into bottomBarStore was
re-writing barComponent on every reactive tick — every change to
openApps, locale or activeSceneId redirected through .set() and
re-assigned the component reference identically.
Add a setProps() method to bottomBarStore that mutates only barProps,
and split the workbench effect in two: a registration effect that
fires on the scenes-empty/non-empty transition, and a props effect
that pushes fresh data without touching barComponent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the raw JSONL response with a zip container:
events.jsonl — one SyncChange per line, as before
manifest.json — formatVersion, schemaVersion, userId, eventCount,
eventsSha256, apps, timestamps, schemaVersionMin/Max
Single DB pass: events.jsonl is written while a sha256 hasher tees
every byte of the uncompressed JSONL. The manifest lands as a second
zip entry after the stream closes, so eventsSha256 is filled without
rescanning.
Integrity-check on the restore side becomes trivial (re-hash the
decompressed events.jsonl and compare). Signature over manifest.json
is deferred to a later phase; sha256 already catches corruption.
Client-side: default filename + UI label updated to .mana. Fetch flow
is unchanged — browser gets a zip blob and triggers a download.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each open workbench app card previously mounted its full ListView with
its own Dexie liveQuery on initial render — so 5+ open apps meant 5+
parallel IndexedDB reads and 5+ async chunk fetches before first paint,
even though only the 1-2 cards in the horizontal viewport are visible
at a time.
PageCarousel now wraps each card in an IntersectionObserver-driven
gate. The first card mounts eagerly so paint isn't gated on observer
callback timing; the rest swap in a fixed-size placeholder until they
enter the viewport (with 50% horizontal overshoot so the next card on
either side is ready before the user scrolls to it). Mount is sticky
— once a card has been instantiated we keep it resident, since
re-running a liveQuery and re-fetching its chunk costs more than
keeping the DOM tree around.
Affects all three carousel users: workbench /, /todo, /contacts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Recovering three files dropped when a parallel terminal reset past the
original M1 commit:
- cmd/server/main.go: register GET /backup/export outside billingMiddleware
- lib/api/services/backup.ts: browser-side downloadBackup() helper
- settings/my-data/+page.svelte: "Backup & Wiederherstellung" section
Pairs with the earlier backup handler + schema_version work already on
main (79996f946). With this commit the endpoint is actually reachable
end-to-end and the download button works.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- sync_changes gains schema_version column (default 1, idempotent ADD)
- Change/Changeset carry schemaVersion; server refuses > MaxSupported
- server->client changes now carry eventId + schemaVersion so the
restore path can dedup via eventId and route through a migration
chain keyed on schemaVersion
- backup JSONL gains schemaVersion per line
Pre-M2 clients (omit the field) are treated as v1 for compatibility.
This is the stability contract we commit to before launch: once v1
events are in the wild, all future builds must replay them forward.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The BYOK vault commit (db8c2574d) reverted the layout to its pre-perf
shape while integrating initByok(). Restore the perf optimizations from
b196e7782 and slot initByok() into the Phase A-idle block alongside the
other side-effect initializers.
Verified bundle impact (perf vs pre-perf parent):
- (app)/+layout chunk: 42 KB → 26 KB gzip (-38%)
- 7 lazy chunks split out (modals on demand, toasts after idle)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 4-5 of BYOK. Wires the BYOK backend into the running app and
gives users a UI to manage their keys.
Data layer (_byokKeys table, v16 schema):
- Encrypted at rest via user master key (wrapValue/unwrapValue)
- NOT in SYNC_APP_MAP — keys stay device-local on purpose
- Tracks per-key usage: count, total tokens, cumulative USD cost
byokVault API (lib/byok/):
- listAll() / listMeta() — decrypt on read
- create() — encrypts + auto-handles isDefault collisions
- update() — label/model/isDefault edits (not the key itself)
- delete() — soft-delete
- recordUsage() — called from backend's onUsage callback
- getForProvider() — resolver helper
initByok() wires the ByokBackend into the shared orchestrator at
app startup. The resolver picks the most-recently-used provider's
key by default. Usage callback updates the key's counters + cost
via estimateCost().
Settings page (/settings/ai-keys):
- List existing keys with provider, label, model, usage stats
- Add form: provider picker, label, API key (password input),
model selector with provider defaults, isDefault toggle
- Inline edit for label + model
- Set-as-default action
- Soft-delete with confirmation
- Gracefully handles locked vault state
Companion chat dropdown already picks up the new 'byok' tier
from ALL_TIERS — user can select BYOK directly as KI-Modus.
Total BYOK implementation: ~1500 LOC across 12 files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1-3 of BYOK support. Introduces a 5th LLM tier 'byok' that
routes to user-provided API keys via direct browser fetches.
shared-llm additions:
- LlmTier extended with 'byok' (rank 3, between mana-server and cloud)
- ByokBackend: LlmBackend implementation that delegates key lookup
to an app-provided resolver callback, then dispatches to the right
provider adapter
- 4 provider adapters:
- OpenAI (gpt-5, gpt-4o, o1 family)
- Anthropic (Claude Opus/Sonnet/Haiku 4.6) with CORS header
- Gemini (2.5 Pro/Flash) — REST API with different message format
- Mistral — OpenAI-compatible, reuses shared openai-compat adapter
- Pricing table for 20+ models with USD per 1M tokens
- estimateCost() + formatCost() helpers
Keys stay device-local (IndexedDB in next phase). Browser-direct
fetches mean keys never touch Mana's server.
Updates two existing tier maps (memoro DetailView, SourceBadge) to
include the new tier.
Planning doc at docs/architecture/BYOK_PLAN.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the shared ThemePage component inside the Themes workbench
panel with a custom compact layout better suited to the narrow
(~300px) panel context. ThemePage was designed for a full-width
desktop route and reads as noisy/overloaded in a panel.
Mode selector (Hell/Dunkel/System) — primary-fill active state with
white icon+text (was subtle shadow-sm that barely registered in dark
mode), fill-weight icons when active, equal-width pill buttons in a
shared muted container.
Theme cards (Option D — "Farbton-Karte") — swap the 2×5 overlapping
color-dot preview for a large 16:10 gradient (primary → secondary in
the effective mode), theme name overlaid bottom-left with text-shadow,
subtle dark-overlay at the bottom for readability, white check badge
in the corner when active, 2px primary border + glow ring for the
active state. Hover lifts the card 1px. Renders all 8 variants
(default + extended) in a uniform 2-column grid.
Wallpaper tabs (Farben/Bilder/Upload + scope toggle) — restyle via
scoped :global() overrides to match the mode selector: muted pill
container, primary-fill active state, muted-foreground inactive.
Previously these used .bg-surface + .shadow-sm which was nearly
invisible against the panel background.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Homepage/workbench TTI was dominated by the layout synchronously booting
event-bridge, streak tracker, LLM queue, memoro watcher, dashboard store,
shared-uload and reminder scheduler before first paint, plus statically
importing 7 modals/toasts/banners that are rarely-to-never visible on
initial render.
- Keep critical path inline: local-store init (manaStore/tag/link),
unified sync engine + billing, guest-mode setup.
- Move side-effect streams, projection workers and telemetry to
requestIdleCallback (with setTimeout fallback).
- Dynamically import KeyboardShortcutsModal, OnboardingWizard,
GuestWelcomeModal, SessionWarning, EncryptionIntroBanner,
SuggestionToast, NudgeToast. Modals fetch on first demand; toasts
mount after idle so their transitive deps (automationsStore,
day-snapshot projection, streaks, crypto gate) don't land in the
initial chunk.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dropdown at the top of the chat with options: Auto, Keine, Browser,
Mana-Server, Cloud. Selecting a tier sets
settings.taskOverrides['companion.chat'] so the choice only affects
the Companion, not other LLM tasks. "Auto" clears the override and
lets the orchestrator pick the user's preferred tier.
Also shows the current auto-selected tier inline so the user knows
what Auto resolves to, e.g. "Auto (Browser)".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Admins can now grant Cloud Sync to users without charging credits. Gifted
rows carry is_gifted=true plus gifted_by/gifted_at audit columns; the
billing cron skips them, and /activate and /deactivate refuse to touch
them. New endpoints POST/DELETE /api/v1/admin/sync/:userId/gift.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the ad-hoc local-first + server-fallback pattern with the
shared LlmOrchestrator, giving the Companion Chat full access to
the 4-tier system (none/browser/mana-server/cloud) and its privacy
+ user-preference enforcement.
New companionChatTask (lib/llm-tasks/companion-chat.ts):
- name: 'companion.chat'
- minTier: 'browser' (no rules fallback — needs an LLM)
- contentClass: 'personal' (allows server/cloud if user opted in;
NOT 'sensitive' because the chat isn't restricted to browser-only,
but the user can set it per-task via taskOverrides)
- requires: { streaming: true }
Engine changes:
- callLlm() now delegates to llmOrchestrator.run(companionChatTask, ...)
- Still preloads the local model when browser tier is available so
the UI can show download progress
- isCompanionAvailable() now asks llmOrchestrator.canRun() which
considers user settings + backend readiness + consent gates
User benefits:
- Tier-selector in the PillNav now applies to Companion Chat
- Users can force cloud/server/browser per-task via settings overrides
- Cloud tier only runs when cloudConsentGiven is set
- Privacy: content marked 'sensitive' in other tasks (Journal etc.)
is still restricted to browser/rules — Companion respects the
same orchestrator so privacy invariants hold consistently
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The PWA was configured end-to-end in vite.config.ts and built the
manifest + service worker correctly, but neither was ever loaded by
the browser — no <link rel="manifest"> in the HTML and no script
registering the generated registerSW.js. Chrome therefore never fired
beforeinstallprompt, no install icon appeared in the URL bar, and the
hand-rolled PwaUpdatePrompt hung on navigator.serviceWorker.ready
because no SW had been registered.
Changes:
- Render pwaInfo.webManifest.linkTag into <svelte:head> in the root
layout so Chrome finds the hashed manifest.
- Replace the hand-rolled SW-update logic in PwaUpdatePrompt with
useRegisterSW() from virtual:pwa-register/svelte — it registers the
worker (immediate: true) and exposes reactive needRefresh +
updateServiceWorker stores that match registerType: 'prompt'.
- Add triple-slash refs to vite-plugin-pwa/info and /svelte in
app.d.ts for the virtual module types.
- Set manifest.id = startUrl in @mana/shared-pwa so Chrome doesn't
warn and keeps the install identity stable across start_url edits.
- Keep devEnabled: false and expand the comment: the 2026-04-08
dreams mic-button bug and the /offline navigateFallback both
misbehave when Workbox precache doesn't run under vite dev. Test
the install flow via `pnpm build && pnpm preview` instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the gap where the Companion module wrote messages directly to
IndexedDB without participating in the Domain Event stream — chat
activity, tool invocations and conversation creation were invisible
to the Event Stream page, Goals, Streaks, and Memory layer.
New events (3 types):
- CompanionConversationStarted: emitted on chatStore.createConversation
- CompanionMessageSent: emitted on user/assistant messages (skips
empty tool plumbing messages)
- CompanionToolCalled: emitted in engine.runCompanionChat after every
tool execution, with tool name, source module, success/failure,
latency in ms, and error message on failure
Event Stream page updated with icons (ChatCircle, Robot) and German
labels for the three new event types so they appear inline with all
other domain activity.
Now possible (future iterations):
- Goals like "5x Companion-Chat pro Woche"
- Streaks for daily Companion usage
- Tool performance analytics ("create_task hat 3% Fehlerrate")
- Memory facts about which tools the user uses most
Total: 70 event types, 51 tools across 31 modules.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 8 system pages as first-class workbench apps (Settings, Themes,
Profile, Admin, API Keys, Help, Feedback, Subscription) so they can be
opened as side-by-side panels next to other apps instead of requiring
a full-page route switch. Existing routes remain as fullscreen
fallback/deep-link targets.
Group the AppPagePicker by 5 categories (Companion, Leben, Arbeit,
Kreativ, System) with collapsible sections; System is collapsed by
default. Search still works as a flat fuzzy match across all apps.
Category assignment lives in a central map so registerApp() calls stay
unchanged — unmapped apps fall back to System, which surfaces
miscategorization at a glance.
Remove profile-data and theme-picker duplication from Settings (both
are separate workbench apps now): Settings defaults to 'Allgemein' and
passes showTheme={false} to GlobalSettingsSection; SettingsSidebar
accepts a categories override so the workbench version hides Profile.
Fix Cannot-read-'subscribe'-of-undefined crash in mood/sleep/body/
stretch ListViews when opened in the workbench: replace getContext
(which is only set by the route +layout.svelte) with direct query-hook
calls, matching the goals/companion pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Five fixes from observed chat where user asked to complete two
tasks by title but the LLM had no way to find their IDs:
1. Tool result history: messagesToLlm() now includes previous
tool_result messages as "[Previous tool result]" entries so
the LLM can reference IDs/data from earlier turns.
2. Bare JSON tool call fallback: extractToolCall() now also
matches bare {"name":..., "params":...} JSON without the
```tool fence — the LLM kept dropping the fence.
3. IDs in list message: list_tasks now formats each entry as
"• [abc123] Title" so the LLM has the ID alongside the title.
4. New complete_tasks_by_title tool: case-insensitive substring
match, completes all matches at once. Handles "erledige beide
sicher sicher tasks" without needing IDs.
5. System prompt updates: explains the [id] bracket convention,
warns the LLM to NEVER show raw IDs to users, and references
the new tool for title-based completion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes for the chat where the user asked "show my tasks" and the
LLM responded with "I don't have direct access to a dynamic task list":
1. New list_tasks tool with title/dueDate/priority for each task,
filterable by open/completed/overdue/today/all (default: open).
The message field returns a markdown bullet list so the LLM can
pass it through directly.
2. System prompt rewritten to explicitly tell the LLM:
- WHEN to call which tool (concrete examples for common questions)
- "Erfinde keine Daten" — don't make up info, call a tool
- Tool format must be JUST the JSON block, no preamble
Common questions now have explicit mappings:
- "Welche Tasks?" → list_tasks
- "Wie viel Wasser?" → get_drink_progress
- "Welche Termine heute?" → get_todays_events
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three UX fixes for the Companion Chat:
1. Markdown rendering: assistant messages now render as HTML via
marked (gfm + breaks). **bold**, _italic_, lists, code blocks,
links all work. Custom CSS for Mana theme integration.
2. Loading status: while the local Gemma model downloads/loads
(first use is ~500MB), the placeholder bubble shows "Modell
wird geladen... 42%" with a spinning icon instead of just a
blank "thinking" state.
3. Streaming feedback: the placeholder bubble appears immediately
when sending and shows tokens as they stream from the LLM,
rendered as markdown in real-time. Auto-scrolls on each chunk.
The pulse animation on the streaming markdown gives a subtle
"this is generating" hint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Final two TODOs resolved — the Companion Brain backlog is now empty.
Goal Editor (GoalEditor.svelte):
- Modal with event type picker (13 options), count/sum mode,
optional filter field/value, target value/period/comparison
- Integrated into Goals ListView with "Eigenes" button alongside
the existing "Vorlage" template picker
- Creates custom goals via goalStore.create()
Incremental Streaks (rewritten streaks.ts):
- Persistent _streakState table replaces the 90-day lookback scan
- 6 streak definitions: water goal, tasks, meals, workout, journal,
meditation — each triggered by specific domain events
- Event bus subscription marks streaks active on matching events
- markActive() is O(1): read state → check if today already active
→ increment or reset based on consecutive day check
- useStreaks() reads from _streakState (single table scan, no
per-day queries) instead of 270+ queries worst case
- startStreakTracker/stopStreakTracker wired into layout lifecycle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>