Wires mana-ai into the existing observability stack so tick throughput,
plan-failure rates, planner latencies, and snapshot refresh health are
visible in Grafana + Prometheus, and the service's uptime surfaces on
status.mana.how under the "Internal" section.
- `src/metrics.ts` — prom-client Registry with `mana_ai_` prefix.
Counters: ticks_total, plans_produced_total, plans_written_back_total,
parse_failures_total, mission_errors_total, snapshots_new/updated,
snapshot_rows_applied_total, http_requests_total.
Histograms: tick_duration_seconds (0.1–120s), planner_request_
duration_seconds (0.25–60s), http_request_duration_seconds (0.005–10s).
- `src/index.ts` — HTTP middleware labels every request by
method/path/status; `/metrics` serves the Prometheus text format.
- `src/cron/tick.ts` — increments counters + wraps the tick with
`tickDuration.startTimer()`. Snapshot stats fold through.
- `src/planner/client.ts` — wraps `complete()` in a latency histogram
timer so planner tail latency shows up separately from tick duration.
- `docker/prometheus/prometheus.yml` —
1. New `mana-ai` scrape job against `mana-ai:3066/metrics` (30s).
2. `/health` added to the `blackbox-internal` job so uptime shows on
status.mana.how alongside mana-geocoding.
- `scripts/generate-status-page.sh` — friendly label for the new probe:
`mana-ai:3066/health` → "Mana AI Runner" (generator already iterates
`blackbox-internal`, no other changes needed).
- `package.json` — prom-client ^15.1.3
All 17 Bun tests still pass; tsc clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sync status, user menu (bar-mode + overlay fallback) and logout now use
the shared Pill component like the rest of PillNavigation. All pill
styling now lives in a single place.
- Pill gains an escape-hatch `data?: Record<string, string>` prop for
arbitrary data-* attributes (used by the user-menu trigger which is
click()-ed via querySelector from the consuming layout) and a
bindable `element` binding for imperative focus/positioning
(replaces the old bind:this={userMenuTrigger}).
- Remove the now-dead inline .pill / .glass-pill / .logout-pill /
.pill-label / .pill.active / .pill.icon-only CSS from PillNavigation
(all living in Pill.svelte now). ~110 lines of CSS gone.
- The mobile override that forced 44px min-height on .pill is also gone;
Pill sizes are controlled via the size prop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the O(N sync_changes) LWW replay in every tick with an
incremental snapshot table refresh. Each tick now applies only the
delta since the last run, then runs a single indexed SELECT on the
snapshot table to find due missions.
- `db/migrate.ts` — idempotent migration. Creates `mana_ai` schema and
`mana_ai.mission_snapshots` table on boot. Partial index on
active+nextRunAt powers the tick's "due" query.
- `db/snapshot-refresh.ts`
- `refreshSnapshots(sql)` one-pass: joins sync_changes and snapshots
on (user_id, mission_id), picks out pairs whose source max
created_at exceeds the snapshot cursor. Per-pair refresh wrapped
in `withUser` for RLS scoping on the source SELECT.
- Bootstrap: missing snapshot rows seed from a full replay of their
mission's history; subsequent ticks apply only the delta.
- Delete tombstones purge the snapshot row.
- `db/missions-projection.ts` `listDueMissions` — single SELECT against
`mana_ai.mission_snapshots` with an indexed WHERE. Dropped the legacy
cross-user scan + per-user two-phase read (unused now). `mergeAndFilter`
stays for its existing test coverage.
- `cron/tick.ts` calls `refreshSnapshots` before `listDueMissions` and
logs when the refresh actually applied rows. No behaviour change
externally.
- `index.ts` awaits `migrate()` on boot (top-level `await` — Bun
supports it natively).
Closes the last item on the AI-Workbench roadmap's "future work" list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PillNavigation rendered three near-identical inline pill blocks (prepended
elements, main nav items, appended elements). Consolidate onto the Pill
component so the visual base stays in lockstep with the bottom-stack bars.
- Extend Pill with size='sm'|'md'. sm = 36px with 18px icons (PillNav
style); md = 44px with 20px icons (bar pills, default).
- Move the icon-only padding override into Pill itself.
- Extract the Mana-Logo SVG (duplicated inline) to ManaLogoIcon.svelte.
- Replace the three inline pill loops in PillNavigation with <Pill size='sm'>.
Mana-logo and iconSvg cases ride the `leading` snippet. onClick vs href
disambiguation is collapsed into a single Pill call per item.
- Remove the now-unreachable .pill-icon scoped CSS that was only meaningful
for the removed inline SVGs (Phosphor icon sizing comes from the size
prop).
Net: ~70 lines removed from PillNavigation.svelte without changing the
render output. Bar-mode triggers (sync / ai / theme / user) still render
inline because their logic is too entangled with activeBarId — leave for
a follow-up.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each iteration bucket now carries a Revert button that undoes every AI
write attributed to that iteration. Closes the last open Workbench
feature in the roadmap.
- `data/ai/revert/inverse-operations.ts` — pluggable registry mapping
event types to their undo actions. Ships with inverses for the five
most common proposable outcomes:
* TaskCreated → tasksStore.deleteTask
* TaskCompleted → tasksStore.toggleComplete (back to incomplete)
* CalendarEventCreated → eventsStore.deleteEvent
* PlaceCreated → placesStore.deletePlace
* DrinkLogged → drinkStore.deleteEntry
Events with no registered inverse are tallied as `skippedUnsupported`
— user knows to handle those manually rather than the service
silently doing nothing.
- `data/ai/revert/revert-iteration.ts` — orchestrator. Filters
`_events` by `actor.iterationId + actor.missionId`, sorts
newest-first (so a completion unwinds before the underlying task
deletion), applies each inverse, returns `RevertStats` summary.
- Workbench UI: Revert button with confirm dialog on every bucket.
Shows "X zurückgenommen · Y nicht unterstützt · Z fehlgeschlagen"
result alert.
- 5 unit tests cover: happy path, unsupported types, failure isolation,
user-event skipping, newest-first ordering.
With this, the AI Workbench has full audit + undo semantics: user sees
everything the AI did, can approve/reject at stage time, and can roll
back approved actions after the fact.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CommandBar was a near-duplicate of QuickInputBar's InputBar with the UX
of a Cmd+K modal. Only arcade still used it. Migrate arcade onto the
existing GlobalSpotlight (hosted by PillNavigation) so there is a single
Cmd+K modal across all mana apps, then remove CommandBar entirely.
Arcade changes (games/arcade/apps/web/src/routes/(app)/+layout.svelte):
- Merge commandBarQuickActions into spotlightActions (nav-level items)
- Convert handleCommandBarSearch into a ContentSearcher that returns the
game list grouped under a single 'Spiele' category
- Drop the standalone Cmd+K handler; PillNavigation + GlobalSpotlight
handle the shortcut
- Remove the <CommandBar> render; add `contentSearcher` +
spotlightPlaceholder to <PillNavigation>
shared-ui cleanup:
- Delete packages/shared-ui/src/command-bar/ (CommandBar.svelte,
CommandBar.types.ts, index.ts)
- Drop CommandBar / CommandBarItem from the public @mana/shared-ui export
- Delete docs/central-services/COMMAND-BAR.md (stale)
No more duplication: highlight + debounce live in search-core, and the
only remaining Cmd+K surface is GlobalSpotlight.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both QuickInputBar (InputBar.svelte), CommandBar.svelte, and GlobalSpotlight
were duplicating syntax highlighting and the 150ms search debounce. Pull
these into a new `packages/shared-ui/src/search-core/` module so the two
input surfaces stay in sync on feel and matching rules.
- search-core/highlight.ts — HighlightPattern type, locale-aware
getHighlightPatterns(), and the shared highlightText() (HTML-escape +
span wrap). Patterns were previously in quick-input/highlightPatterns.ts
+ inline in CommandBar.svelte.
- search-core/config.ts — SEARCH_DEBOUNCE_MS = 150. Used from InputBar,
CommandBar, GlobalSpotlight, and apps/mana web SearchEngine.
- quick-input/highlightPatterns.ts + types.ts become thin back-compat
re-exports.
- Public surface: @mana/shared-ui now exports getHighlightPatterns,
highlightText, SEARCH_DEBOUNCE_MS, and the HighlightPattern type.
No UX change. UIs still live in their own files (per earlier split
recommendation: shared backend, separate surfaces).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the "cross-user scan" caveat on the mission read path. The
earlier implementation pulled every aiMissions row server-wide and
partitioned by user_id in memory — fine for a pre-launch single-user
deploy, not a cross-user infrastructure.
New flow:
1. `listMissionUsers(sql)` — one cross-user DISTINCT query. This is
the ONLY surface that still reads across users; documented as
requiring BYPASSRLS on the service's DB role (or ownership without
FORCE).
2. `listDueMissionsForUser(sql, userId, now)` — RLS-scoped via
`withUser(sql, userId, tx => ...)` just like the write path in
`iteration-writer.ts`. Defense-in-depth: even if the SELECT mis-
filters, RLS drops any row whose user_id doesn't match the session
setting.
3. `listDueMissions(sql, now)` — two-phase composition of the above.
The LWW merge + due-filter logic is factored out into a pure
`mergeAndFilter(rows, userId, now)`. Fully unit-tested (6 Bun cases):
active-due happy-path, future nextRunAt, non-active state, delete
tombstone, multi-row LWW merge, userId stamping.
Matches the pattern already in use for writes (`db/connection.ts:withUser`
+ `db/iteration-writer.ts`). Docstring on `listMissionUsers` spells out
the remaining BYPASSRLS dependency so ops knows what role the service
needs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract Pill.svelte as the single visual primitive (44px, icon+label,
active/primary/danger variants) used by PillDropdownBar and TagStrip.
PillNav keeps its own internal .pill class (36px, icon-only-oriented).
- Extract phosphor-icon-map.ts to deduplicate the icon lookup tables
that previously lived inline in PillDropdownBar.
- Unify bar slot heights in (app)/+layout.svelte: 56px PillNav,
64px for tags / quickinput / tabbar / dropdown-bar. Remove debug
outlines. Collapse bottom-stack gap so bars sit flush below PillNav.
- SceneAppBar wrapped in 64px slot, scene-pill/app-tab 40px to match.
- Enforce single-bar policy: opening one bar closes the others.
- QuickInputBar strip-down: remove leading CheckSquare icon and trailing
nav-toggle snippet; bar is pure search input now.
- Move user-menu (last PillNav pill) to bar-mode with short content:
Einstellungen, Light/Dark/System segmented, Theme, Logout.
- Swap tabs nav icon from Columns to Cards for better readability.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Planner reads `mission.iterations[].userFeedback` — not
`pendingProposals.userFeedback` — so feedback stored only on the
proposal row never reached the next plan. rejectProposal now also
appends to the matching iteration's `userFeedback` (merging with any
existing reasons via "\n· " bullets) when the proposal carries
missionId + iterationId.
Lazy imports the mission store to avoid a proposal↔mission cycle.
Best-effort: proposal rejection still succeeds even if the bubble fails.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rejecting a ghost-card proposal now reveals an inline textarea. Trimmed
feedback lands in `proposal.userFeedback` where the next Planner
iteration already reads it via iteration history — the AI learns from
concrete rejection reasons without needing a settings UI.
- Click "Ablehnen" → textarea + Cancel/Reject buttons replace the
approve/reject row
- Empty submission still rejects (userFeedback stays undefined so the
Planner sees "no reason given" rather than an empty string)
- `rejectingId` + `rejectDraft` are per-inbox local state; only one
reject form is open at a time
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
One-line drop-in per module page. Each module's proposals now render as
ghost cards inline wherever the user already expects to see that
module's content — no separate approvals inbox to hunt for.
Covers all four remaining modules with proposable tools in
AI_PROPOSABLE_TOOL_NAMES:
- calendar → create_event
- places → create_place, visit_place
- drink → undo_drink
- food → (auto-tools only today; the inbox is ready for future
propose-class food tools)
Now the only modules with proposable tools NOT showing the inbox are
those that don't have their own /route (all covered by todo, calendar,
places, drink, food).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 1–9 scroll to the Nth open app on the workbench homepage; 0 opens the
app picker.
- q/w/e toggle the bottom bars (workbench tabs / search / tags); r opens
the user-menu PillDropdownBar (expanding the PillNav first if needed);
t toggles the PillNav visibility.
Adds a `data-user-menu-trigger` hook on the user pill so the layout can
drive the menu bar programmatically without duplicating its config.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Makes the webapp's AI policy and the server's tool allow-list physically
impossible to drift. Adds the missing entries the guard caught on first
run: `complete_tasks_by_title`, `visit_place`, `undo_drink` now have
parameter schemas server-side too.
- `packages/shared-ai/src/policy/proposable-tools.ts`
- `AI_PROPOSABLE_TOOL_NAMES` as `const` array + literal union type
- `AI_PROPOSABLE_TOOL_SET` for set-membership checks
- Webapp `DEFAULT_AI_POLICY` derives its `propose` entries from the
shared list via `Object.fromEntries(...)` — adding a tool there is now
a one-line change in `@mana/shared-ai`
- mana-ai `AI_AVAILABLE_TOOLS`: module-load assertion compares its
hardcoded names against `AI_PROPOSABLE_TOOL_SET` and throws with a
pointed error on drift (extras in one direction, missing in the
other). Service refuses to start on mismatch — better than silent
degradation.
- Bun test (`tools.test.ts`) runs the same contract plus sanity checks
(non-empty description, required params carry docs). Vitest policy
test adds the symmetric check on the webapp side.
All three runtimes now green: webapp 66/66, shared-ai 2/2,
mana-ai 9/9 Bun tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mission-Input-Picker now surfaces open tasks + upcoming calendar events
alongside notes / kontext / goals. When the foreground runner runs, the
corresponding resolvers decrypt the records client-side and hand real
content (title + due date / event time + description) into the Planner
prompt.
- `tasksResolver` + `tasksIndexer` — reads unencrypted subset + decrypts
via `decryptRecords('tasks', …)`. Picker shows only OPEN tasks (not
completed ones) to keep the list relevant. Resolver output is
`[status]{ · fällig date}{ \n description}` — terse by design.
- `calendarResolver` + `calendarIndexer` — similarly decrypts events;
picker prioritizes upcoming events (sorted by startIso), shows
near-term times as "bald: YYYY-MM-DDTHH:MM" to make recency obvious
- Both tables are encrypted client-side — server-side mana-ai resolvers
remain intentionally absent (per the privacy contract in
`services/mana-ai/src/db/resolvers/types.ts`)
With this, a user can create a Mission like "plan my week" and link a
few tasks + calendar events as context; the in-browser planner sees the
full picture (decrypted), while the off-tab runner still plans from
objective + concept only for those missions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Plugs plaintext-safe Mission context into the Planner prompt per tick.
Before this, `resolvedInputs: []` was always passed — the LLM only saw
the mission's concept + objective. Now goals (the only plaintext
category of linked inputs today) resolve and land in the prompt.
Privacy constraint is explicit and documented: tables in the webapp's
encryption registry (notes, kontext, journal, dreams, …) arrive at
`sync_changes.data` as ciphertext — the master key lives in mana-auth
KEK-wrapped and never reaches this service. Resolvers for encrypted
modules therefore don't exist server-side; missions referencing them
should use the foreground runner which decrypts client-side.
- `db/resolvers/types.ts` — ServerInputResolver contract
- `db/resolvers/record-replay.ts` — single-record LWW replay
(tighter WHERE than `missions-projection.ts`, used by all resolvers)
- `db/resolvers/goals.ts` — reads `companionGoals` via replayRecord,
mirrors the webapp's default goalsResolver output shape
- `db/resolvers/index.ts` — registry with `registerServerResolver` /
`unregisterServerResolver` / `resolveServerInputs`. Seeds `goals`.
Drift-tolerant: missions pointing at unregistered modules silently
skip those inputs.
- `cron/tick.ts` — wires `resolveServerInputs(sql, m.inputs, m.userId)`
into the planner input; updates the outdated "stubbed" comment
5 Bun tests over the registry (handled + unhandled + thrown +
mixed cases + seeded default).
Future: expand to plaintext tables if/when more land (habits without
free-text, dashboard configs, tags), or introduce a decrypt-via-auth
sidecar if users opt into server-side access to encrypted content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Completes the off-tab AI pipeline. mana-ai now writes produced plans
back to `sync_changes` as a server-sourced Mission iteration; the webapp
picks it up on next sync and translates each PlanStep into a local
Proposal via the existing createProposal flow. User sees the resulting
ghost cards in the matching module's AiProposalInbox with full mission
attribution.
Server (mana-ai v0.3):
- `db/connection.ts` — `withUser(sql, userId, fn)` RLS-scoped tx helper
mirroring the Go `withUser` pattern (SET LOCAL app.current_user_id)
- `db/iteration-writer.ts`
- `planToIteration(plan, id, now)` — shared-ai AiPlanOutput → inline
MissionIteration with `source: 'server'` + status='awaiting-review'
- `appendServerIteration(sql, input)` — INSERT sync_changes row with
op=update, data={iterations: [...]} + field_timestamps + actor
JSONB={kind:'system', source:'mission-runner'}
- `cron/tick.ts` — after parse success: build iteration, append to
mission.iterations, persist via appendServerIteration. Stats now
include `plansWrittenBack`.
Actor union:
- `packages/shared-ai/src/actor.ts` + webapp actor: `system.source` gains
`'mission-runner'` so the server's own writes are attributed correctly
and distinguishable from projection/rule writes
Webapp:
- `data/ai/missions/server-iteration-staging.ts`
- `startServerIterationStaging()` subscribes to aiMissions via Dexie
liveQuery; on each Mission update, walks iterations looking for
`source='server'` entries that haven't been staged yet
- For each such iteration: creates a Proposal per PlanStep under
`{kind:'ai', missionId, iterationId, rationale}` so policy + hooks
fire correctly
- Writes proposalIds back into plan[].proposalId + status='staged' so
other tabs and app restarts skip re-staging
- Idempotent: in-memory `processedIterations` Set + durable
proposalId marker
- Wired into (app)/+layout.svelte alongside startMissionTick
- 3 unit tests: translate server iteration → proposal, skip
already-staged, ignore browser iterations
Full pipeline now: user creates Mission in /companion/missions →
mana-ai tick picks it up → calls mana-llm → parses plan →
writes iteration → synced to webapp → staging effect creates
proposals → user approves in /todo (or any module) → task lands with
`{actor: ai, missionId, iterationId, rationale}` attribution.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Service now produces plans end-to-end for due missions. Takes the
shared prompt/parser from @mana/shared-ai, calls mana-llm's
OpenAI-compatible endpoint, parses + validates the response against a
server-side tool allow-list.
- `src/planner/tools.ts` — hardcoded subset of webapp tools where
policy === 'propose'. Mirror of `DEFAULT_AI_POLICY` in the webapp;
drift just means the server doesn't suggest newly-added tools
(graceful degradation). Contract test between the two lists is a
sensible follow-up.
- `src/cron/tick.ts`
- Iterates due missions, builds the shared Planner prompt per mission,
parses the LLM response, logs the resulting plan
- Per-mission try/catch so one flaky LLM response doesn't abort the
queue; stats now track `plansProduced` + `parseFailures`
- `serverMissionToSharedMission()` converts the projection shape to
the shared-ai Mission type at the boundary
- `resolvedInputs: []` today — the Planner sees concept + objective +
iteration history only. Full resolvers (notes/kontext/goals via
Postgres replay) land alongside write-back in the next PR.
- No write-back yet: the plan is logged but not persisted to
`sync_changes`. Write-back needs an RLS-scoped helper mirroring
mana-sync's `withUser` pattern — tracked explicitly as the remaining
open piece in CLAUDE.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single source of truth for AI Workbench types shared between the webapp
(Vite/SvelteKit) and the server-side mana-ai Bun service. Prevents the
two runtimes from drifting on prompt shape or mission structure.
- `@mana/shared-ai` package:
- `actor.ts` — Actor union (user | ai | system) + helpers, mirrors the
webapp's runtime type so server-side consumers parse incoming actors
without re-declaring
- `missions/types.ts` — Mission, MissionCadence, MissionInputRef,
MissionIteration, PlanStep, MissionState. Adds optional
`iteration.source: 'browser' | 'server'` to distinguish foreground
vs server-produced iterations (groundwork for proposal write-back)
- `planner/prompt.ts` — `buildPlannerPrompt` pure function
- `planner/parser.ts` — `parsePlannerResponse` strict JSON validator
- Vitest smoke tests (2) cover prompt → parse round-trip + unknown-
tool rejection
- Webapp:
- `missions/types.ts` re-exports from shared-ai, keeps webapp-local
`MISSIONS_TABLE` constant + `planStepStatusFromProposal` bridge
- `missions/planner/{types,prompt,parser}.ts` become re-export stubs
so existing imports keep working unchanged
- Existing webapp tests (60) continue to pass — the wire code didn't
move, just its home
Next: mana-ai service imports buildPlannerPrompt/parsePlannerResponse
from shared-ai + wires mana-llm + writes iteration back as a
'source=server' row (tracked in services/mana-ai/CLAUDE.md).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Background Hono/Bun service that scans mana_sync for due Missions and
will plan them via mana-llm without requiring an open browser tab.
Complements the foreground `startMissionTick` in the webapp.
v0.1 scope — scaffold that's deployable, boots cleanly, and reads real
data. Execution write-back is tracked as the next PR so we don't commit
a half-baked proposal-sync design.
Shipped:
- Hono app on :3066 with `/health` + service-key-gated `/internal/tick`
- `src/db/missions-projection.ts` — field-level LWW replay of
`sync_changes` for appId='ai' / table='aiMissions' → live Mission
records. Mirrors the webapp's `applyServerChanges` semantics against
Postgres instead of Dexie.
- `src/db/connection.ts` — bounded `postgres.js` pool (max 4, idle 30s)
- `src/cron/tick.ts` — overlap-guarded scheduler, `runTickOnce()` also
reachable via HTTP for CI/ops triggering
- `src/planner/client.ts` — mana-llm HTTP client shape
(OpenAI-compatible `/v1/chat/completions`)
- `src/middleware/service-auth.ts` — X-Service-Key gate, no end-user JWTs
reach this service
- Dockerfile + graceful SIGTERM shutdown (stops timer + releases pool)
Not yet implemented (documented in CLAUDE.md with design trade-offs):
- Prompt/parser server-side copies — today they live in the webapp.
Recommended next step: extract `@mana/shared-ai` package.
- Input resolvers for notes / kontext / goals — need projections or a
mana-sync internal endpoint
- Plan → Mission-iteration write-back + how proposals get back to the
user's device (leaning option (a): server writes iterations, the
webapp's sync effect translates them into local Proposals)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the cross-device attribution loop. When another device pushes a
change with `actor: { kind: 'ai', missionId, … }`, the receiving device
now persists that attribution onto the record so the Workbench timeline
and per-module ghost badges render the same way they would on the
originating device.
- `readFieldActors()` sibling helper next to `readFieldTimestamps` for
reading the per-field actor map off a record
- `applyServerChanges`:
- Insert-new-record: stamp every field with `change.actor`, set
`__lastActor` on the whole record
- Insert-as-upsert: stamp only the winning fields (same LWW condition
as the timestamp merge), update `__lastActor` to the change actor
- Field-level update: same per-field + whole-record stamping
- Pre-actor clients (change.actor undefined) fall back to USER_ACTOR so
legacy rows still have a valid stamp
- All three paths also add the new hidden keys to their "skip" lists so
incoming payloads can't smuggle old bookkeeping fields through
With this, the full pipeline is cross-device:
Device A (AI writes) → meta.actor + __lastActor + pendingChange.actor
mana-sync (Go) → persists actor JSONB on sync_changes row
Device B (sync pull) → applyServerChanges re-stamps __lastActor +
__fieldActors from the incoming change
Device B (Workbench) → renders the AI's activity from `_events` with
correct rationale + mission context
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Matches the wire contract the Go server just learned to persist. Every
PendingChange now carries the actor through the Dexie row into the POST
payload; SyncChange on the receiving side accepts an opaque actor blob.
- `sync.ts`
- `SyncChange.actor?: Actor` on the wire type; documented as opaque +
back-compat with pre-actor clients
- `PendingChange.actor?: Actor` — the pending-changes row already gets
an actor stamped by the Dexie hook, the type now reflects it
- `isValidSyncChange` accepts actor as an object or undefined, never
asserts internal shape (the payload is opaque by design)
- Push payload includes `actor: p.actor` alongside the other fields
- `module-registry.test.ts` — `pendingProposals` added to INTERNAL_TABLES.
It's a local-only staging table that intentionally does NOT sync
(approved writes run the underlying tool, which syncs normally).
Follow-up still open: when applyServerChanges writes a record from an
incoming change, stamp `__lastActor` + `__fieldActors` from the incoming
actor so the Workbench timeline attributes cross-device writes
correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds an opaque JSON `actor` column alongside the existing field_timestamps
so cross-device consumers can distinguish user / ai / system writes. The
server never parses the shape — it just stores and re-emits the blob the
webapp stamped in its Dexie hook.
- `sync/types.go` — Change.Actor as json.RawMessage with omitempty; nil
for pre-actor clients so wire remains backward-compatible
- `store/postgres.go`
- Migrate: CREATE TABLE includes `actor JSONB` for fresh DBs;
ALTER TABLE ADD COLUMN IF NOT EXISTS actor JSONB for existing ones
(idempotent, safe to re-run)
- RecordChange signature takes json.RawMessage; pgx writes nil as NULL
- All three SELECT paths (GetChangesSince, GetAllChangesSince,
StreamAllUserChanges) return actor, Scan into ChangeRow.Actor
- ChangeRow.Actor added with doc noting "missing = user" consumer rule
- `sync/handler.go` — Change.Actor threaded through HandleSync →
RecordChange, and populated on both changeFromRow (pull/POST replies)
and convertChanges (SSE stream)
- Tests: roundtrip of an AI-actor payload + omitempty verification for
pre-actor clients. All existing tests still pass.
Webapp types still need `actor?: Actor` on SyncChange + PendingChange to
match the wire, and applyServerChanges needs to stamp __lastActor /
__fieldActors from incoming changes for Workbench attribution on other
devices — both tracked as separate follow-ups.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Derived-state writes should be attributed to the projection subsystem,
not to whoever triggered the upstream event. `_streakState` is local-
only today so no cross-device user-visible effect, but once any derived
table joins sync this is the only correct model.
- `markActive` and `ensureSeeded` now run under
`runAsAsync({ kind: 'system', source: 'projection' }, …)`
- Sets the pattern for future projections (DaySnapshot, correlations, …)
to follow verbatim when they start writing persistently
Closes one of the Step-1 follow-ups tracked in
COMPANION_BRAIN_ARCHITECTURE §20. Remaining:
- mana-sync Go + Postgres migration for the `actor` field
- rule-engine to wrap its future writes the same way (no writes today)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Single-page view of everything the AI has done, grouped by mission
iteration. Closes the "what did the assistant actually touch today?"
question that used to require raw event-log spelunking.
- `data/ai/timeline/queries.ts`
- `useAiTimeline({ missionId?, module?, limit? })` — reactive live
query over `_events`, filtered to `actor.kind === 'ai'`. Over-fetches
by 3x and client-filters because `actor.kind` isn't indexed; cap at
500 entries keeps it cheap.
- `bucketByIteration(events)` — groups events sharing
`actor.missionId + actor.iterationId` into a single visual unit so
the rationale reads once per iteration rather than once per event.
Pure function, fully unit-tested.
- `routes/(app)/companion/workbench/+page.svelte`
- Buckets rendered chronologically with mission-link header + rationale
- Per-event row shows module + event type + payload title + deep-link
back into the module
- Module dropdown filter + `?mission=…` query-string for mission-scoped
views (linked from /companion/missions detail header)
- `/companion` sidebar + missions detail header now link to the Workbench
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the "blind Planner" gap: users can now attach context records to
a Mission, and the Runner resolves them through the existing resolver
registry before calling the Planner. The LLM sees the actual linked
note content, not just the mission objective.
- `data/ai/missions/input-index.ts` — sibling registry to input-resolvers.
Resolvers turn a ref into prompt text (Runner path); indexers list
candidates for the picker UI (create-form path). Same shape, different
direction; keeps modules decoupled from the AI layer on both ends.
- `data/ai/missions/default-resolvers.ts` — registers indexers for
notes (title + content preview, capped at 200), kontext (the singleton
doc), goals (title + progress). Co-located with the resolvers so the
two halves stay in sync.
- `components/ai/MissionInputPicker.svelte` — drop-in picker: module
selector → candidate list → chip-style selected display. Binds
directly to `MissionInputRef[]` so forms use it as a single control.
- `/companion/missions` — picker wired into the create form between
Konzept and Cadence; detail view's meta block now lists the linked
inputs so users can see what context the Planner will see.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create, review, and control AI Missions from the app. Closes the last
UX gap in the end-to-end pipeline — users no longer need the Dexie
console to drive the Runner.
- `data/ai/missions/queries.ts` — `useMissions({ state? })` live query
+ single-mission `useMission(id)`. Decryption-ready via `decryptRecords`
wrapper (no-op today, future-proof when/if Missions get added to the
crypto registry).
- `routes/(app)/companion/missions/+page.svelte`
- Inline create form: title + objective + markdown concept + cadence
picker (manual / interval-minutes / daily-hour). Weekly + cron are
wired in state but not exposed until a richer picker is worth it.
- List / detail layout, responsive to narrow viewports.
- Detail view: run-now button (invokes runMission with productionDeps),
pause/resume/complete/delete lifecycle actions, iteration history
with per-iteration feedback form.
- Uses shared-icons + scoped CSS with theme-token fallbacks.
- `routes/(app)/companion/+page.svelte` — footer nav links to
/companion/missions and /companion/rituals from the chat sidebar.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Connects the dependency-injected Runner to the real LlmOrchestrator and
drives it on a foreground tick in the app shell. Registers sensible
default input resolvers so Missions linked to notes / kontext / goals
work without per-module opt-in.
- `data/ai/missions/setup.ts`
- `productionDeps` wires `aiPlanTask` through `llmOrchestrator.run`
- `startMissionTick(intervalMs = 60_000)` kicks an immediate run then
schedules `runDueMissions` on interval. Idempotent + overlap-guarded
so a slow LLM run can't pile up ticks.
- `stopMissionTick` clears the interval for teardown / HMR.
- `data/ai/missions/default-resolvers.ts` — resolvers for notes (title +
decrypted content), kontext (singleton markdown), goals (progress
projection). Registered once when the tick starts.
- `(app)/+layout.svelte` wires startMissionTick into the idle-phase init
block alongside startEventStore / startStreakTracker / etc., and
stopMissionTick into the teardown path.
System is now end-to-end runnable in the browser: create a Mission with
cadence 'interval', wait for the tick, see proposals appear in
/todo's AiProposalInbox. Missions UI (create/edit form) still open.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Executes one iteration end-to-end: resolve Mission inputs → build the
policy-filtered tool allowlist → invoke the Planner → stage each
PlannedStep as a Proposal (or auto-run if policy says so) → finalize
the iteration with summary + status.
- `data/ai/missions/runner.ts`
- `runMission(id, deps)` runs a single iteration. Planner + stageStep
are injected so the Runner is unit-testable without a live LLM.
- `runDueMissions(now, deps)` scans for active missions past their
nextRunAt and runs each once. Safe to call on a foreground tick.
- Reuses the iteration id returned by `startIteration` so
`finishIteration` updates the same row (fixed a dup-id bug the
tests caught).
- `data/ai/missions/input-resolvers.ts` — registry: modules register a
resolver at init, Runner looks up by module name. Missing resolvers
degrade gracefully to "fewer inputs", never crash a run.
- `data/ai/missions/available-tools.ts` — exposes only tools the AI
policy rates non-`deny`. Defence-in-depth with the executor + parser.
overallStatus derivation:
0 steps → 'approved' (no-op run is valid)
all steps failed → 'failed'
any step staged (proposal id) → 'awaiting-review'
all steps ran auto → 'approved'
Planner throw is caught and recorded as a failed iteration — one bad
mission can't stall the queue.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Turns a Mission (concept + objective + linked inputs + iteration history)
into a structured plan of tool-call proposals via the shared
LlmOrchestrator.
- `data/ai/missions/planner/types.ts` — AiPlanInput, AiPlanOutput,
PlannedStep, ResolvedInput, AvailableTool
- `data/ai/missions/planner/prompt.ts` — pure builder producing system +
user messages. System prompt enforces a strict fenced-JSON contract and
lists available tools with parameter schema. User prompt injects the
mission content, resolved input records, and the last 3 iterations
(especially any userFeedback so the planner can course-correct).
- `data/ai/missions/planner/parser.ts` — strict parser with a
discriminated ParseResult union. Rejects unknown tools, missing
rationale, malformed shape. Tolerates missing optional fields.
- `llm-tasks/ai-plan.ts` — aiPlanTask LlmTask, minTier 'browser',
contentClass 'personal'. On parse failure returns an empty plan with
an explanatory summary rather than throwing, so the Runner can record
a failed iteration without killing the queue.
No Runner yet — the planner is pure (input in, plan out). Runner (next
commit) will resolve inputs from modules, invoke the task, stage each
PlannedStep as a Proposal under the AI actor, and update the Mission
iteration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>