# Nebula Nodes — Session Handoff (2026-04-20)

> **How to use this doc:** paste the full contents into the first message of the next session. The new agent should be productive after reading this + `.planning/handoff-2026-04-18.md` (deeper history) + tail of `~/.claude/activity/2026-04-19.log` and `~/.claude/activity/2026-04-20.log`.

This handoff layers on top of `.planning/handoff-2026-04-18.md`. Read that first for milestone history; this one captures the 2026-04-19/20 sessions where the chat panel went from WIP to fully round-tripped.

---

## TL;DR

- **Three v1 pillars unchanged:** BYOK, nodes working, Claude chat fluency. Everything below serves those.
- **Chat ↔ canvas is now fully bidirectional.** Library drags, param edits, manual edges, deletions, and dynamic universal nodes all push to cli_graph; loaded `.nebula` files import through cli_graph too. Claude can see and modify everything the user does.
- **11 commits shipped this session** on `feat/3d-mesh-support`. Tree is clean. Ready to PR or keep stacking.
- **Mac app still deferred.** Don't propose Mac work.

---

## Critical working rules (read every session)

1. **Verify-Before-Reporting** (`~/.claude/CLAUDE.md`). Fetch canonical primary sources before stating facts. WebSearch → discover URLs. WebFetch/curl → extract facts.
2. **Research Protocol** (`~/.claude/rules/research-protocol.md`). `_hot.md` → `_index.md` → topic note. Write back to all four when done.
3. **Activity Logging** (`~/.claude/rules/activity-logging.md`). Log via `bash ~/.claude/scripts/activity-log.sh write '...'` — never raw heredocs.
4. **Dual-param architecture** (memory `feedback_dual_params.md`). Nodes with FAL + direct routes use `sharedParams` / `falParams` / `directParams`. Never merge.
5. **v1 essentials frame** (memory `project_v1_essentials.md`). Before adding any feature, ask: does it strengthen BYOK, node correctness, or Claude fluency? If no, park it.
6. **Mac app gating** (memory `project_mac_app_gating.md`). Don't start Mac-native work until OSS web is solid + 10–20 dogfood sessions pass.
7. **Backend has no hot-reload.** Every change to `chat_session.py`, `main.py`, handlers, or anything under `backend/` requires `lsof -ti:8000 | xargs kill; cd backend && python -m uvicorn main:app --port 8000`. Frontend HMRs automatically.

---

## What got done in 2026-04-19/20

### Chat panel reached v1 (commits `54a8a0b`, `1081124`, `05f454d`)

- Backend `/ws/chat` WebSocket bridges `claude -p --resume <sid>` per turn, parses stream-json into normalized events. `NEBULA_DISABLE_QUICK=1` hard-gates `nebula quick` so Claude can only use graph mode.
- Frontend chat panel: draggable header, 8 resize handles (4 edges + 4 corners with clamp-aware position math), code-block parser with `Apply to @nX` button that pastes prompts back into the triggering text-input.
- Toolbar gained `Chat` toggle and `Clear` button (confirms, wipes canvas + DELETEs `/api/graph` so phantom cli_graph state goes away).
- Connection popup: visible X close button, per-category collapse, `screenToFlowPosition` for cursor-accurate node placement.
- Node library: collapsed-by-default categories with chevrons + counts. Utility now first via `CATEGORY_ORDER`.
- Model nodes: draggable `@nX` id chip into chat, Enhance button on text-input that fires `nebula:chat-send` event with `sourceNodeId`, hover-reveal Download button on image/video/audio/mesh previews (blob fetch for cross-origin downloads).
- `.nebula` v2 file format persists outputs + state so saved graphs reopen with their generated images intact (commit `33f910a`).

### Critical bug train: cli_graph round-trip (commits `2e8436d`, `a7d092c`, `139a8e8`, `edad26f`, `b99c8a7`, `1eb1b5c`)

The chat panel "worked" but Claude saw a different canvas than the user. Six fixes closed the loop:

1. **`2e8436d` Loaded `.nebula` files visible to Claude.** New `POST /api/graph/import` atomically clears cli_graph and rebuilds with fresh short IDs + preserved positions + outputs. CLIGraph.add_node accepts position + outputs. Frontend `handleLoad` routes through it; graphSync rehydrates.
2. **`a7d092c` Library drags / param edits / manual connects flow to cli_graph.** `addNode` async → POST. `updateNodeData` debounces (250ms) PUT for cli nodes. `onConnect` pushes to backend when both endpoints are cli. New `POST /api/graph/node-and-connect` for atomic create+connect (ConnectionPopup). graphSync merge dedupes edges by connection identity rather than id so optimistic UUID edges get replaced cleanly.
3. **`139a8e8` Node + edge deletions sync.** `DELETE /api/graph/node/{id}` (cascades edges) and `DELETE /api/graph/edge` (4-endpoint match). Frontend `onNodesChange`/`onEdgesChange`/`deleteNode` push deletions for cli-origin items. Dynamic universal nodes also push to cli_graph now (optimistic UUID + background POST + UUID→short-ID swap on success). Merge spreads `existing.data` BEFORE `cliNode.data` so dynamic-only fields (`dynamicInputPorts`, `dynamicParams`, `providerMeta`, `modelId`) survive. keyStatus computed for every merged node.
4. **`edad26f` Reject unknown param keys.** `_validate_params` on POST/PUT node + node-and-connect endpoints raises 400 with "did you mean…?" hint based on case+underscore-insensitive near-match. Underscore-prefixed (`_previewUrl`, `_output_image`) always allowed. Universal/dynamic nodes skip validation. Primer gained VERIFY AFTER CREATE rule: after `nebula create --param` or `nebula set`, run `nebula graph` and confirm.
5. **`b99c8a7` argparse `--param` chain bug.** `nargs='*'` was making each `--param` flag OVERWRITE the previous one — Claude's `--param model=X --param aspect_ratio=9:16 --param imageSize=1K` only landed `imageSize=1K` because argparse silently dropped the first two. Switched to `action='append' nargs='*'` + flatten in `parse_kv_list`. Both documented styles now work. **This was the root cause of every "Claude said he set X but Inspector shows default" bug.**
6. **`1eb1b5c` Walked back the ITERATION INTENT rule.** I'd added "modify in place and re-run" as the default for variant requests. That overrode the older PRESERVE EXISTING WORK rule and Claude started destroying prior outputs. Replaced with BRANCH, DON'T OVERWRITE: default for "make this X / try it in 9:16 / make a version" is create a new node connected to the same inputs. Modify in place ONLY on explicit replace signals (replace, overwrite, change this one, get rid of the old one). Includes a worked CORRECT vs WRONG example for the iPhone-aspect case.

### Primer (chat_session.py NEBULA_SYSTEM_PRIMER) currently teaches:

- Always run `nebula graph` first, never `nebula quick` (hard-gated)
- Branch by default; reuse existing input nodes; only modify in place on explicit replace language
- Suppress YouMind sign-off and other third-party skill footers
- Aspect-ratio vocabulary: iPhone/phone/portrait/mobile → 9:16, square → 1:1, YouTube → 16:9, cinema → 2.39:1, etc.
- Verify after create: re-run `nebula graph` and check the param landed; flag the casing gotcha (Nano Banana mixes `aspect_ratio` snake + `imageSize` camel in the same node)
- Don't refuse media requests, don't dump long tool output into chat

---

## Current state

### Branch

`feat/3d-mesh-support` — clean tree, 11 commits ahead of where we started this morning. Last 5:

```
1eb1b5c fix(primer): walk back iteration-in-place rule
b99c8a7 fix(cli): --param flag now accumulates across repeated use
edad26f fix(graph): reject unknown params + tell Claude to verify after create
139a8e8 feat(graph): sync deletions to cli_graph; push dynamic universal nodes too
a7d092c feat(graph): route library drags, param edits, and manual connects through cli_graph
```

### What works end-to-end now

- User drags library node → cli_graph entry → Claude sees it
- User types in text-input → debounced PUT → cli_graph reflects it
- User draws manual edge between cli nodes → cli_graph reflects it
- User loads a `.nebula` file → import endpoint rebuilds cli_graph with positions + outputs
- User deletes node/edge → backend deletes too, no resurrection on next graphSync
- Dynamic universal nodes (openrouter/replicate/fal-universal) push to cli_graph with UUID→short-ID swap
- `/clear` chat command + Clear toolbar button wipe both frontend AND cli_graph
- Enhance button → chat composes message with downstream node refs → Claude rewrites prompt → Apply button pastes back

### Known limitations (intentional, all documented in code)

- **Param-typing race during concurrent edits.** Debounced PUT means up to 250ms of typed chars could be stomped if a graphSync arrives mid-typing. In practice graphSync only fires on backend state changes which are rare during pure typing.
- **Undo/redo for cli operations is fragile.** `pushUndo` snapshots local state but the backend has its own state. Undoing a `nebula create` doesn't DELETE on backend; next graphSync would resurrect. Workaround: use Clear button.
- **Auto-layout is naive.** New cli nodes go at `maxX + 300`. Branching three Nano Banana nodes off one text-input gets wide and ugly.

---

## What's next — pick one

Under the v1 essentials frame:

### High-leverage (direct pillar hits)

1. **Drag node output image into chat as vision block.** Currently the `@nX` id chip drops as text, which Claude resolves via CLI. A second drag mode (drag the IMAGE preview, not the id chip) would attach the actual image bytes so Claude can SEE the output and reason about it visually. Direct hit on pillar #3 (Claude fluency) — unlocks "what's wrong with this image?" / "make it warmer" / multi-modal critique loops. Backlog item from `.planning/backlog/chat-panel-followups.md` #2.
2. **BYOK audit end-to-end.** Walk every handler's `get_api_key()` call, confirm Settings UI exposes a field for each `envKeyName`, missing-key errors are user-friendly. Pure pillar #1. No one's done a full sweep.
3. **Reference library design spike.** User reconfirmed wanting a helper library (color palettes, prompt templates, reference images) that BOTH Claude and nodes can consume. Sharpened scope is in backlog. Needs a design doc before implementation.

### Medium

4. **Auto-layout for branched graphs.** The current `maxX + 300` heuristic produces wide messy layouts when a single input fans out to multiple model nodes. Either teach the backend to lay out children in a vertical stack or run a real DAG layout (dagre port, ~200 lines). Open questions parked in backlog #3.
5. **Skill-table drift lint.** A script that diffs `backend/execution/sync_runner.py`'s endpoint map vs `.claude/skills/fal/SKILL.md` routing table. Would have caught the seedance-v1-5 endpoint bug. Probably 30 min of work.
6. **Node correctness dogfood.** A "last successful run timestamp per node id" matrix to expose dead handlers before users hit them. The FAL gap report caught broken endpoints; this catches the rest.

### Low (won't help v1)

7. Drag-to-chat v2 with structured refs (covered by #1)
8. Helper library full implementation (covered by #3)
9. Visual zones — deferred
10. Mac app — deferred

### Recommended: **#1 (drag-image-to-chat as vision)**

Direct hit on Claude fluency. Once Claude can SEE outputs, every iteration loop gets stronger ("this looks washed out — try `image_size=2K`"). Builds on the drag-id pattern that already works. Estimated 1–2 hours.

---

## Decisions awaiting user input

1. **Next workstream.** User said "what's next on our todo" at end of last session. They expect a recommendation + reasoning.
2. **Branch strategy.** `feat/3d-mesh-support` now contains chat panel + cli_graph round-trip + 3D mesh work. Probably worth renaming or PRing soon — branch name no longer matches scope.
3. **Param-race mitigation.** Currently there's a 250ms window where rapid typing can be stomped by an unrelated graphSync. Leave it (rare in practice) or add a "user-editing" flag that suppresses param updates from cliNode for that node?

---

## Known quirks

- **Backend needs manual restart** for any `backend/` change (no `--reload`).
- **Stale-output 404s** when frontend holds an old `/api/outputs/...` URL past the file's lifetime. Harmless but ugly.
- **FAL i2v source-image compatibility** — see `.claude/skills/fal/skills/bytedance__seedance-2.0__fast__image-to-video.md`. Not every public URL works; v3.fal.media WebPs are safest.
- **`settings.json`** gitignored. Don't commit test API keys.
- **`.planning/private/`** gitignored (Mac app spec).
- **YouMind sign-off** comes from `~/.agents/skills/nano-banana-pro-prompts-recommend-skill/SKILL.md`. Suppressed via primer rule, not by removing the skill.

---

## Key file locations

### Backend
- `backend/main.py` — FastAPI app, WebSockets, REST. New endpoints: `/ws/chat`, `/api/graph/import`, `/api/graph/node-and-connect`, DELETE `/api/graph/node/{id}`, DELETE `/api/graph/edge`. Param validation in `_valid_param_keys` + `_validate_params`.
- `backend/services/chat_session.py` — `claude -p` subprocess wrapper + NEBULA_SYSTEM_PRIMER. Edit primer here for behavioral changes.
- `backend/services/cli_graph.py` — in-memory graph state. Now has `remove_node`/`remove_edge`/position+outputs in `add_node`.
- `backend/services/cache.py` — execution cache (TTL 1hr, keyed on nodeType+params+inputs).
- `backend/cli/__main__.py` — argparse setup. `--param` uses `action='append' nargs='*'` after the chain bug fix.
- `backend/data/node_definitions.json` — 93 node schemas.

### Frontend
- `frontend/src/components/panels/ChatPanel.tsx` — draggable, resizable from all corners, code-block parser + Apply button.
- `frontend/src/store/graphStore.ts` — async addNode + addNodeAndConnect + addDynamicNode, debounced param sync, deletion push, edge dedup by connection identity. `CLI_ID_RE = /^n\d+$/` distinguishes cli vs frontend-only.
- `frontend/src/components/Canvas.tsx` — uses `screenToFlowPosition` for drops.
- `frontend/src/components/ConnectionPopup.tsx` — atomic create+connect via `addNodeAndConnect` for cli origin nodes.
- `frontend/src/components/nodes/ModelNode.tsx` — Enhance, id chip, download overlay, inline text-input renderer.
- `frontend/src/components/panels/Toolbar.tsx` — Save/Load/CLI/Clear/Fit/Nodes/Chat/Settings.

### Planning
- `.planning/handoff-2026-04-18.md` — prior handoff (FAL gap report era).
- `.planning/handoff-2026-04-20.md` — this doc.
- `.planning/backlog/chat-panel-followups.md` — helper library spike notes (sharpened 2026-04-19).
- `.planning/fal-node-gap-report.md` — FAL audit (still relevant for future param additions).

### Skills (project)
- `.claude/skills/{fal,meshy,gemini,runway}/`

### User memory
- `~/.claude/projects/-Users-justinperea-Documents-Projects-nebula-nodes/memory/MEMORY.md`

### Activity log
- `~/.claude/activity/2026-04-19.log` (today's work — graphSync merge, polish pass, cli_graph round-trip)
- `~/.claude/activity/2026-04-20.log` (any work captured today before this handoff)

---

## Recommended first action for next session

1. Read this handoff + `.planning/handoff-2026-04-18.md` for deeper context.
2. Tail today's activity log: `tail -100 ~/.claude/activity/2026-04-20.log` (if exists) and `~/.claude/activity/2026-04-19.log`.
3. Confirm branch is clean with `git status`. Expect `feat/3d-mesh-support` clean tree.
4. Ask the user which "what's next" item to start. If they defer, start with #1 (drag-image-to-chat as vision) — it's the highest-leverage v1 pillar #3 work.

---

## User working style (calibrated through this session)

- Short replies, no emojis. Never recap the whole session at end of turn — one or two sentences max.
- Direct answers when asked a yes/no. "Yes" / "No" / "It's a Z bug" beats "It might be a Z bug, let me investigate."
- Verify before claiming. When something looks wrong, GET / curl / read state before guessing root cause.
- Trusts execution details, confirms big architectural choices ("do both" was explicit). When in doubt, present 2-3 options with tradeoffs and let them pick.
- Lowercase casual tone in short messages. Doesn't mean anything — just register.
- Likes seeing live verification ("ran curl, here's the output") not "this should work."
- Prefers commits as logical chunks, not one mega-commit. 5-commit decomposition pattern works well.
- Keeps planning docs updated alongside code (CLAUDE.md global rule). Always update backlog/handoffs when scope changes.
