Commit Graph

57 Commits

Author SHA1 Message Date
Joseph Doherty 22db9f3554 test: bump schema_version assertion to 8 after 0008_group_node migration 2026-04-26 15:49:25 -04:00
Joseph Doherty 6b726b2a4a merge: T38 relationship-seed service 2026-04-26 15:49:03 -04:00
Joseph Doherty e58cdbd527 merge: T37 guest_added/guest_removed event handlers 2026-04-26 15:49:03 -04:00
Joseph Doherty b69a74e69b merge: T36 group_node schema + projector handlers 2026-04-26 15:49:03 -04:00
Joseph Doherty c6b3531c64 feat: relationship-seed service for first-co-appearance prompt 2026-04-26 15:47:12 -04:00
Joseph Doherty a0d7debce5 feat: group_node schema + projector handlers 2026-04-26 15:46:16 -04:00
Joseph Doherty a1b4e251c5 feat: guest_added / guest_removed event handlers 2026-04-26 15:46:09 -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