* refactor: unify bot loop with runSession(), drop initialize/timeout
Unify the duplicated session lifecycle in processMessage() and
sendToAgent() into shared helpers:
- baseSessionOptions: computed once, not duplicated
- getSession(): 3-way create/resume/fallback in one place
- persistSessionState(): save agentId/conversationId/skills
- runSession(): send with CONFLICT retry, deduplicated stream
Also:
- Drop session.initialize() -- SDK auto-initializes on send()
- Drop withTimeout() wrapper -- SDK should own timeouts
- sendToAgent() shrinks from 98 to 20 lines
- processMessage() shrinks from 437 to ~250 lines (delivery stays)
- Net -187 lines (1013 -> 825)
All recovery logic preserved: pre-send attemptRecovery(),
CONFLICT catch + retry, empty-result orphan recovery.
Fixes#197
Written by Cameron ◯ Letta Code
"Make it work, make it right, make it fast." -- Kent Beck
* fix: narrow conversation-not-found fallback to 404/missing errors
Codex review caught that runSession() was retrying with createSession()
on ANY send error when agentId exists, not just conversation-missing
cases. Auth/network/protocol errors would incorrectly fork conversations.
Now only retries on 404 or error messages containing "not found" /
"missing" / "does not exist". Other errors propagate immediately.
Written by Cameron ◯ Letta Code
"Be conservative in what you send, be liberal in what you accept." -- Postel's Law
* fix: persist agent ID eagerly on creation, not deferred to result
Codex review caught that agent/conversation IDs were only saved in the
stream result handler. If createAgent() succeeded but send/stream failed,
the ID was lost and the next message would create a duplicate agent.
Now: getSession() persists the agent ID + runs first-run setup (name,
skills) immediately after createAgent(). persistSessionState() only
updates conversation ID on result.
Written by Cameron ◯ Letta Code
"Fail fast, but don't forget what you learned." -- unknown
* fix: persist conversation ID after send, before streaming
Codex review caught that conversationId was only saved on stream result.
If streaming disconnected or aborted before result, the next turn would
fall back to resumeSession(agentId) (default conversation) instead of
resuming the actual conversation -- forking context.
Now saved immediately after successful send(), matching the pre-refactor
behavior where it was saved after initialize().
Written by Cameron ◯ Letta Code
"The best time to save state was before the failure. The second best time is now." -- adapted proverb