Commit Graph

52 Commits

Author SHA1 Message Date
Joseph Doherty b69a74e69b merge: T36 group_node schema + projector handlers 2026-04-26 15:49:03 -04:00
Joseph Doherty a0d7debce5 feat: group_node schema + projector handlers 2026-04-26 15:46:16 -04:00
Joseph Doherty b8335895e1 docs: add Phase 2 implementation plan with parallel-safe waves
13 tasks across 6 waves (1, 2, 3, 4a, 4b, 5). Designed for parallel
subagent execution where file-disjointness allows.

Waves 1, 2, 4a, and 5 each contain 2-3 tasks that touch disjoint files
and can be dispatched concurrently via the Agent tool with
isolation: "worktree". Waves 3 (drawer guest support) and 4b (multi-
entity turn flow) are single-task because they touch hot files
(_drawer.html, turns.py) that cannot be safely co-modified.

Plan covers:
- T36: group_node schema + handlers (new migration 0008)
- T37: guest_added / guest_removed event handlers (modifies world.py)
- T38: relationship-seed service ("have they met?")
- T39: interjection classifier service
- T40: multi-entity state-update coordinator (6 directed pairs)
- T41: multi-witness memory write helper
- T42: drawer guest add/remove UI + render
- T43: multi-entity prompt assembly (extends T18)
- T44: multi-entity turn flow (rewrites post_turn)
- T45: multi-entity per-POV summaries on scene close
- T46: witness filter cross-coverage tests
- T47: bot_reset cascades to guest references
- T48: Phase 2 documentation update

Plan also documents:
- Worktree-per-subagent dispatch pattern using Agent isolation flag
- Merge ordering per wave (file-disjointness = conflict-free merges)
- Failure recovery (cancel failed parallel task, re-dispatch as solo)
- Conflict prevention checklist (verify Files sections disjoint per wave)

Tasks file (.tasks.json) carries dependency DAG with `blockedBy` and
`parallelGroup` so a future executing-plans run can dispatch correctly.

NOT EXECUTING. Plan only.
2026-04-26 15:37:07 -04:00
Joseph Doherty d161e7b8e9 feat: cap narrative response length + tune sampling
Bot replies were running long (4 paragraphs of action+dialogue beats
per turn) because we never set max_tokens on the narrative call. Three
tunable knobs now in Settings (set in data/config.toml to override):

- narrative_max_tokens: int = 400
  Hard cap on each generated response. ~400 tokens ≈ 1–2 short
  paragraphs. Drop to 200 for terse banter, bump to 800+ for longer
  scenes.

- narrative_temperature: float = 0.85
  Sampling temperature. 0.7 = grounded/consistent (slightly stiff),
  0.85 = creative-but-in-character (default), 1.0 = wide variety,
  >1.0 = often off-the-rails.

- prompt closing instruction now nudges: "Keep your response to a
  single beat — one or two short paragraphs at most. Don't monologue;
  leave room for the other person to react."

Both turns.py (post_turn) and regenerate.py forward the params to
client.stream(). FeatherlessClient already passes **params through to
the OpenAI-compat endpoint.

Note: temperature doesn't control length — that was a common
misconception. max_tokens is the actual length cap. Lower temperature
makes word choice more predictable (slightly stiffer voice), not
shorter. Both knobs are useful for different goals.
2026-04-26 15:28:08 -04:00
Joseph Doherty f0742dd4f9 fix: use readOnly (not disabled) to lock textarea during stream
The form-submit handler in chat.html was setting
``textarea.disabled = true`` synchronously before the browser actually
serialized the form. Disabled form fields are excluded from
submission, so the request body contained ``prose=""`` even when the
user had typed text — which the server (correctly) rejected with the
new empty-prose 400. Net effect: typing "hello" + Send gave a "prose
cannot be empty" error.

Switched to ``readOnly``: same UX (user can't edit while streaming)
but the field IS submitted. The unlock path now also clears the
textarea and refocuses for the next turn.
2026-04-26 15:23:06 -04:00
Joseph Doherty 52555e0455 fix: reject empty prose on turn submit
Empty submission was producing a blank user_turn event in the log and
firing the LLM stream anyway — the bot would invent a response from the
kickoff context alone, producing a monologue with no user input. Two-
layer fix:

- Browser: add `required` to the prose textarea in chat.html so the
  form refuses to submit empty.
- Server: 400 in post_turn when prose.strip() is empty. Defense in
  depth — if a client bypasses the textarea attribute (custom UI,
  curl, etc.), the server still rejects.

Verified live: POST with empty body returns 400; POST with whitespace-
only returns 400; chat shell renders the textarea with required.
Full suite: 168 passed.
2026-04-26 15:20:02 -04:00
Joseph Doherty 5c039c8e56 fix: classifier timeout + Featherless concurrency cap
Two related issues blocking real-world use of the kickoff parse:

1. Classifier calls take ~12s end-to-end on Featherless for the
   complex KickoffParse schema (Hermes-3-8B generating ~1.3KB of
   structured JSON). The 10s timeout was firing on most attempts,
   causing all 3 retries to time out and the empty-fallback to render
   with blank form values. Bumping the default
   classifier_timeout_s 10 → 30s gives generous headroom; measured
   p99 is ~13s, so 30s is comfortable.

2. Featherless caps concurrent connections per account (2 on free /
   lower paid tiers). Each turn flow can fire 4–5 calls (parse,
   scene-close detect, narrative stream, two state-update passes)
   plus the background significance worker. Without a gate, we'd
   exceed the cap and fail.

   Added a class-level ``asyncio.Semaphore`` to FeatherlessClient,
   shared across all instances, configured once in lifespan from
   ``Settings.featherless_max_concurrent`` (default 2). Both
   ``generate`` and ``stream`` acquire the semaphore for the duration
   of the call; the stream holds it until the async generator
   completes, so token streaming is correctly accounted for.

Verified live: 4/4 sequential kickoff parses for the same bot all
succeed with real parsed values (previously ~50% blank-fallback).
Full suite: 168 passed.
2026-04-26 15:15:14 -04:00
Joseph Doherty 5aab98e4d7 fix: classifier robustness — schema in prompt, retries, kickoff fallback
The kickoff parse-and-confirm route was 500-ing intermittently because
Hermes-3 + Featherless's response_format={"type":"json_object"} only
guarantees JSON output, NOT a particular schema. The model was inventing
its own field names (sceneTime, entities, settingDetails) instead of
the KickoffParse fields, causing Pydantic validation to fail on both
classify() retries.

Three changes:

1. Include the Pydantic JSON schema in the system prompt so the model
   knows exactly which keys to produce. Affects every classify() call
   (kickoff parse, turn parse, scene-close detect, significance,
   state-update, scene summarize). Strip ```json fences if the model
   wraps its output. Bump retries 2 → 3 (model is stochastic; one extra
   attempt closes most of the remaining gap).

2. parse_kickoff() now passes a default empty KickoffParse so the
   route degrades to a fillable form instead of 500 when the classifier
   ultimately fails. The confirm form is the human-in-the-loop; an
   empty form is strictly better UX than a stack trace.

3. Tests updated: bumped canned-failure arrays from 2 → 3 entries to
   match the new attempt count; renamed kickoff test from
   "raises_when_classifier_fails_twice" to
   "falls_back_to_empty_when_classifier_fails" reflecting the new
   degraded-but-usable behavior.

Verified live with all 3 sample bots (maya/eli/sam) — kickoff route
returns 200 across multiple attempts. Full suite: 168 passed.
2026-04-26 15:03:13 -04:00
Joseph Doherty 12502d6ec7 chore: add scripts/seed_sample_bots.py
Idempotent seeder for three sample bots (Maya — coworker slow-burn,
Eli — live-in partner, Sam — bartender / new connection). Each is a
distinct relational archetype to exercise the system from different
angles. Run from repo root:

    .venv/bin/python scripts/seed_sample_bots.py

Re-running skips ids that already exist. After seeding, walk each bot
through kickoff parse-and-confirm at /bots/<id>/kickoff.
2026-04-26 14:50:06 -04:00
Joseph Doherty 365dacc0d0 chore: post-Phase-1 cleanup — gitignore, packaging, backlog
- .gitignore: add *.egg-info/ so editable installs don't show in git status.
- pyproject.toml: add [build-system] and [tool.setuptools.packages.find]
  scoped to chat*, fixing pip install -e . which was failing on data/
  auto-discovery.
- CLAUDE.md: add Phase 1.5 cleanup backlog section under Phase 1 status,
  capturing the small follow-ups surfaced in implementer reviews
  (open_db refactor, regenerate SSE broadcast, you-activity purge,
  drawer edits for deferred fields, NICE trim order).
2026-04-26 14:39:10 -04:00
Joseph Doherty a302ed427a feat: error banners and first-run navigation flow 2026-04-26 14:33:28 -04:00
Joseph Doherty 0353d592cd feat: streaming UX with Stop, disconnect handling, send-lock 2026-04-26 14:27:39 -04:00
Joseph Doherty 330077afcf feat: transcript display formatting with markdown and OOC styling 2026-04-26 14:22:43 -04:00
Joseph Doherty 8390703b73 feat: nightly DB backups with 14-day retention 2026-04-26 14:18:57 -04:00
Joseph Doherty b9644fad31 feat: periodic snapshots with retention and cold-load fast-path 2026-04-26 14:15:17 -04:00
Joseph Doherty 82be8b3f51 feat: bot reset with hard confirm and event-driven purge 2026-04-26 14:07:56 -04:00
Joseph Doherty 46062973c2 feat: regenerate with edit-then-regenerate inline UX 2026-04-26 14:04:02 -04:00
Joseph Doherty aa0563b4fa feat: rewind with impact preview, pre-rewind snapshot, undo toast 2026-04-26 13:58:20 -04:00
Joseph Doherty b5175aefaa feat: per-POV summary and edge summary update on scene close 2026-04-26 13:53:12 -04:00
Joseph Doherty 0997562e75 feat: scene close on hard signals with manual override 2026-04-26 13:46:14 -04:00
Joseph Doherty db3005fc17 feat: drawer edits with manual_edit event capture 2026-04-26 13:40:40 -04:00
Joseph Doherty 5fc5b8ac23 feat: read-only drawer with scene, activity, edges, memories 2026-04-26 13:35:47 -04:00
Joseph Doherty 3995a8671b feat: FTS5 memory retrieval with witness filter and ranking boosts 2026-04-26 13:30:40 -04:00
Joseph Doherty eb4cdf9cbb feat: async significance pass with auto-pin on score 3 2026-04-26 13:27:25 -04:00
Joseph Doherty a45dabb6ae feat: per-turn memory writes with witness flags 2026-04-26 13:20:43 -04:00
Joseph Doherty e8d24a0875 feat: post-turn state-update pass per present entity 2026-04-26 13:17:07 -04:00
Joseph Doherty 9b45710cb1 feat: narrative streaming via SSE with assistant_turn event 2026-04-26 13:09:31 -04:00
Joseph Doherty 73d8b0c092 feat: prompt assembly with must/should/nice trim tiers 2026-04-26 13:00:00 -04:00
Joseph Doherty a0f5e818ec feat: turn input parser via classifier 2026-04-26 12:53:34 -04:00
Joseph Doherty 656c2558cb feat: per-chat SSE channel and pub/sub 2026-04-26 12:49:41 -04:00
Joseph Doherty e79f4d8d22 feat: chat shell page rendering 2026-04-26 12:39:15 -04:00
Joseph Doherty 0c08745194 feat: top-level nav and chat list view 2026-04-26 12:36:20 -04:00
Joseph Doherty fbb16c86b3 feat: kickoff parse-and-confirm flow with chat creation 2026-04-26 12:28:05 -04:00
Joseph Doherty e44e2bf93f feat: settings page with you-entity authoring 2026-04-26 12:22:00 -04:00
Joseph Doherty 44ea627a8a feat: bot authoring form with bot_authored event 2026-04-26 12:17:06 -04:00
Joseph Doherty a5339fc1d2 feat: kickoff prose parser via classifier 2026-04-26 12:09:17 -04:00
Joseph Doherty ec344064f1 feat: chats, chat_state, containers, scenes, activity tables 2026-04-26 12:03:26 -04:00
Joseph Doherty 30e6648122 feat: memory schema with witness flags and FTS5 index 2026-04-26 11:56:32 -04:00
Joseph Doherty bc97d425ef feat: directed edges with per-turn delta projector 2026-04-26 11:51:15 -04:00
Joseph Doherty 7e6c2985dd docs: fix Task 6 plan snippet: PRAGMA table_info name index is c[1] not c[0] 2026-04-26 11:48:30 -04:00
Joseph Doherty 5e6bbb586c feat: bot and you entity schemas with projector handlers 2026-04-26 11:46:19 -04:00
Joseph Doherty 517fe49aef feat: append-only event log with projector skeleton 2026-04-26 11:42:49 -04:00
Joseph Doherty c2aceffda1 feat: classifier wrapper with retry, timeout, schema-default fallback 2026-04-26 11:38:48 -04:00
Joseph Doherty e627356168 feat: LLMClient protocol with Featherless and mock implementations 2026-04-26 11:35:57 -04:00
Joseph Doherty 67517926aa feat: sqlite migration runner with meta version table 2026-04-26 11:32:32 -04:00
Joseph Doherty 01e6975d20 feat: config loader with toml + env override 2026-04-26 11:28:40 -04:00
Joseph Doherty 4a60171035 feat: project skeleton with health endpoint 2026-04-26 11:23:38 -04:00
Joseph Doherty f0594c24d2 docs: add Phase 1 implementation plan with 36 TDD tasks
Bite-sized, TDD-driven plan covering all v1 deliverables from the
requirements doc §13. Each task has: file paths, failing test,
verify-fail step, minimal implementation, verify-pass step, and a
single commit.

Phases:
- 1A Foundation: project skeleton, config, migrations, LLM client, classifier
- 1B State machine: event log, projector, entities, edges, memory, world
- 1C Authoring: kickoff parser, bot form, settings, kickoff confirm
- 1D Chat (single bot): nav, chat shell, SSE, turn parse, prompt assembly,
  narrative streaming
- 1E Per-turn updates: state-update, memory writes, significance, retrieval
- 1F Drawer & scenes: read-only drawer, drawer edits, scene close, per-POV
  summary
- 1G Rollback: rewind w/ impact preview, regenerate, reset
- 1H Ops & polish: snapshots, backups, display, streaming UX, errors,
  first-run

Companion tasks.json captures dependency DAG for resumable execution.
2026-04-26 11:19:33 -04:00
Joseph Doherty 8a6b48be11 docs: flesh out behavioral specs in v1 requirements (round 2)
Resolves the deferred operational and UX semantics from round 2 of the
brainstorm. Decisions 22–43 in the appendix decisions log.

New / expanded sections:
- §3.3 Classifier failure handling (Pydantic-constrained + retry + schema-
  default fallback, 10s timeout, refusal triggers fallback-model swap).
- §3.4 Edge update granularity (per-turn deltas + per-scene-close summary
  rewrite; all mutations go through edge_update events).
- §4.3 Chat clock format (stored ISO 8601 UTC; displayed friendly relative).
- §5.1 Authoring expanded (voice samples format, trait list as free-form
  phrases, backstory length target).
- §5.4 "You" entity authoring (one-time, shared).
- §6.4 Drawer expanded (v1 editable fields cut: activity, edges, memory;
  read-only: container, identity, witness, structural; manual_edit events).
- §6.5 Activity record specifics (open verb + classifier-extracted props).
- §7.4 Container authoring (parse-and-extend, per-chat scoped).
- §7.5 Guest leaves mid-scene (auto close + new scene with you+host).
- §8.5 Pinning (soft cap 8, score-3 auto-pin, manual pins never auto-evict).
- §10 Rollback expanded with full impact-preview modal, snapshot frequency
  (100 events / 30 min periodic, pre-rewind always), inline regenerate UX
  with edit-then-regenerate.
- §11.1 Significance rubric (0=Routine, 1=Notable, 2=Significant,
  3=Pivotal) with usage and tie-breakers.
- §16 UI Shape & Flow (top-level nav, first-run experience, display
  formatting, streaming UX, error UX).

CLAUDE.md adds a "Behavioral defaults (round 2)" section flagging the
load-bearing rules for future Claude sessions.

§14 Open / Deferred Decisions trimmed to the genuinely-still-open list
(embedding model, vector index choice, prompt templates, search, etc.).
2026-04-26 11:11:46 -04:00
Joseph Doherty 5869f1c5ce docs: lock remaining v1 design decisions
Resolves the open/deferred decisions from the v1 requirements brainstorm:
runtime stack, classifier model, token budgets, OOC marker, data layout.

- Runtime: FastAPI + HTMX + SSE (multi-tab sync is a Phase 1 requirement,
  not a polish item). 127.0.0.1 only, no auth in v1.
- Classifier model: NousResearch/Hermes-3-Llama-3.1-8B with documented
  fallback chain (dolphin-2.9.4-llama3-8b, Meta-Llama-3.1-8B-abliterated).
- Token budgets: 8K hard / 6K soft for narrative, 4K hard for classifier;
  Must/Should/Nice trimming tiers spelled out in §3.2.
- OOC marker locked to ((double parens)), configurable.
- All runtime data lives under <repo>/data/ (DB, backups, snapshots,
  exports, config). Tree is gitignored. CHAT_DB_PATH env var honored.

CLAUDE.md and the requirements doc updated to match. Decisions log in
the requirements doc appendix extended with the new locks (#17–21).
2026-04-26 10:56:51 -04:00