Conversation
Bot memory grew steadily because the grammy runner keeps every in-flight update context alive until its handler promise settles (sink timeout defaults to infinity), and several outbound calls could hang forever: generateText with no deadline, embeddings requests with no Effect.timeout, yt-dlp with no spawn timeout, and ky response body reads that ky's headers-only timeout never bounds. Each pinned context also held a full base64 copy of the message attachments, so every hang added permanent megabytes. - Add a 120s total timeout to Llm.invoke; log invocation errors - Add Effect.timeout to embeddings requests (30s image, 10s query) - Kill yt-dlp after 180s; abort direct video downloads so the temp-dir cleanup always runs - Pass AbortSignal.timeout to ky calls that stream response bodies - Load attachment bytes from S3 only on the AI reply path instead of pinning base64 on every group message context - Clamp the client-controlled inline-query offset to 1000 - Add a 10-minute runner sink timeout that evicts stuck updates and logs their id and type, covering hang sites without per-call deadlines (Telegram API calls, S3 reads)
Replace the legacy bot and model pipeline with an Effect-based model boundary. This centralizes provider selection, telemetry, usage accounting, tool execution, and prompt caching while removing unused application layers.
Rebuild group-chat handling on idempotent Postgres admission, fenced lane runs, a BullMQ wake queue reconciled by a stranded-wake outbox, and an append-only hash-chained conversation context (prompt-cache phases 2-3). Adds the schema migration, CONVERSATION_* env settings, and BullMQ / @prisma/adapter-pg dependencies. Follow-up review pass over the same change set: - fix Cyrillic wake-word regex: ASCII \b can never match Russian text - bound admission retry to 5 exponential attempts instead of forever - collapse repeated error mapping, lane keys, hashes, and prompt renderers into shared helpers; drop unused observedPendingRevision job field - remove speculative verifyTurn chain re-hash per prepare
…d contexts Runs pin to an immutable context generation and reprepare against it. Soft token-cap crossings summarize older complete runs into frozen memory after delivery, hard-cap violations checkpoint before model invocation or block the run, and profile transitions seed a child generation from the frozen envelope. Each attempt persists through prepared -> summarizing -> summarized -> committed with lane locking and fencing at every step; failed hardSafety attempts remain resumable across crashes.
…rrors non-retryable - blockRun now clears the lease, advances processedRevision, and schedules successors in the same transaction; previously a blocked run stayed activeRunId forever and every later message re-claimed it and failed - a frozen request hash mismatch is now a permanent ContextError so the run blocks instead of redriving forever (attemptCount only advances during model invocation); failed() unwraps TransactionError so ContextErrors thrown inside Prisma callbacks keep their classification - checkpoint summarization preserves Model.Error retryability and drain blocks runs whose resumable checkpoint fails permanently - simplify: shared lane lock/fence module, discriminated union for prepareCheckpoint results, shared reply-target renderer, test teardown helpers
Worst-case drains (checkpoint summarization + generation ≈ 240s) outlived the 180s lane lease, letting a second worker reclaim the run and re-invoke the model on it — duplicate provider spend; fencing bounded the damage to database writes only. Lease renewal now rides the existing transactions that open each long stage (model invocation, dispatch, checkpoint summarization), so staleness stays under one lease period without heartbeat timers and crash-recovery latency is unchanged. Also add ARCHITECTURE.md covering engine invariants, and record the tool-budget round semantics as intentional so it stops surfacing as a bug.
Replace the custom Exa API adapter and web_lookup wrapper with Exa's hosted MCP tools. This lets the current model use native search and fetch tools while preserving the existing one-round budget and optional service profile.
ConversationKey owns its wire format and BigInt boundary (format/toDb/fromDb); usage.ts reads Usage.normalizeStep / Usage.aggregate / Usage.upstreamProvider. Run input payload and frozen-request schemas live once in conversation/run-artifacts; the context side decodes a derived projection view tolerant of older rows. verifyPrefix and stableSeed now sit beside extendPrefix in prompt.ts. Declare LiveMessagePayload.repliedText that describeReplyTarget already read.
Each package exposes typecheck alongside lint; root turbo typecheck fans out in dependency order. Starlight lint switches from oxlint to tsc --noEmit, and AGENTS.md prescribes lint followed by typecheck.
Type stored Prisma Json columns as unknown at read boundaries and cast once on write; bind provider.model outside the tryPromise closure so narrowing holds; pass options to dispatchRun to match its signature.
Telegram accepts a fixed reaction emoji set; define it once beside the delivery call that enforces it, type ReactionAction.emoji with it, and reuse it in the reply output schema.
Pass the discovered MCP tools through unwrapped instead of overriding descriptions, drop the system-prompt web-tool section, and map Exa ConfigError to a typed ExaError instead of an unhandled defect.
Config.option still failed startup with ConfigError on a malformed env; catch it to None so generate surfaces the Unavailable error through the normal reply path instead.
The Minimus registry shuts down soon, so nothing can keep pulling from reg.mini.dev. - valkey swaps to the official valkey/valkey:9.1.1-alpine image - classification moves to python:3.14-slim-trixie instead of alpine: PyTorch ships glibc-only Linux wheels (manylinux_2_28, linux_x86_64), so musl cannot load them; slim keeps the same glibc wheel resolution as uv.lock - non-root contract preserved: explicit uid/gid 1000 user keeps the deployment group_add render-group mapping and HF cache volume ownership working - libgomp1/libatomic1/libnuma1 now installed via apt in both stages, replacing the minimus-specific .so copy hack The three bun apps were already on oven/bun alpine images.
The ConversationContext service file carried pure domain logic (checkpoint boundary selection, run-to-transcript projection, summary contract) as loose functions below the service layer. That mixed domain rules with Effect and Prisma plumbing and made the logic reachable only through a database-backed service. Move it into two namespace modules following the existing Lane/Prompt/ConversationKey convention: - Checkpoint (@/context/checkpoint): head/tail boundary resolution over sealed turns, summary instructions, Summary schema - Transcript (@/context/transcript): projection of a finalized run into transcript turn candidates, kind-to-role mapping, known-message-id collection Transaction orchestration (prepare/commit/summarize checkpoint, ensureActiveContext) stays in the service where its Prisma.TransactionClient and service dependencies live. Behavior is unchanged; names dropped their noun prefix to avoid stutter under the namespace.
The extracted namespace modules re-declared database row shapes by hand: Transcript.ProjectionRun duplicated the run include used by appendFinalized, CheckpointTailTurn repeated context-turn columns, and both boundary functions carried an inline structural constraint. Copies like these drift silently when the schema changes, which contradicts the house rule of extracting from generated types with Pick instead of restating them. Derive every row-facing type from the Prisma client, mirroring how OpenCode V2 derives from Drizzle's $inferSelect: - Transcript.Run = ConversationRunGetPayload over the exact include appendFinalized queries - Checkpoint.SealedTurn = ConversationContextTurnGetPayload with its transcript source - Checkpoint.TailTurn and Checkpoint.Attempt as Picks of those models, replacing CheckpointTailTurn and the inline ordinals projectRun now compiles against real columns, so comparisons such as run.status === "failed" are checked against the generated enum. No runtime behavior changes.
Modules previously exported loose top-level symbols and every consumer
invented its own module alias through \`import * as X\`, so the same
module could appear under different names across call sites and nothing
tied a file to the name it was consumed under.
Following the OpenCode V2 convention, each shared module now declares
one canonical \`export namespace <Name>\` block at the source and
consumers import it by name (\`import { Transcript } from
"@/context/transcript"\`). Member access is unchanged, so only import
lines and module wrappers moved.
- Enforce with oxlint import/no-namespace; apps/web keeps
\`import * as React\` per shadcn/ui convention
- ChatReply keeps its eager prompt read as a module-scope const because
top-level await cannot live inside a namespace body
- Disable eslint/no-inner-declarations: namespace members are
module-level declarations, not nested blocks
- Document the rule in AGENTS.md and the effect skill
Finalization used to re-read every stored transcript content blob and type-cast our own JSON writes back to unknown just to rebuild the set of already-projected Telegram message ids. The scan was the only source for those ids because linkedReplyContext targets often have no input row (replies to photos or pre-bot messages), and its cost grew with the full lane history. projectRun now records the id it projects into a nullable source_message_id column, written once from trusted payload and action values; both readers select the column instead of scanning JSON, and collectMessageIds is gone. Content bytes are untouched, so rolling prefix hashes and byte-identical replay are unaffected. The migration backfills existing rows from content JSON and the actions join. Also repair the frozen-request regression fixture: b7cbf92 widened PreparedRequestSchema but left {currentDate} in place, so decode failed before the hash comparison ran; DB-gated tests skip without DATABASE_URL, which hid the red.
The prepared request froze six fields, but only currentDate and sessionId feed a retry: messages were never read back, and the JSON fingerprint/eligibility duplicated columns written in the same transaction. Every run row carried up to 20 rendered messages that no code path consumed. Freeze only what time erodes; rebuild rendering deterministically from the immutable batch inputs. Reply-target membership came from the active context generation's turns, so a hard checkpoint could summarize a target out of the retained tail and silently change D mid-run. Derive it from the append-only lane transcript instead, which stays stable across checkpoints and retries. Eligibility now has one owner: the runs column written at claim time, matching the rule that persisted batch decisions must not be recomputed on retry. Lane keys converted number->BigInt at nearly every helper call; ClaimedRun now carries the DB-shaped key converted once at claim, and ConversationKey.format accepts both id shapes.
Persist attributed user, chat, and topic memories as immutable revisions so each conversation run uses a stable context snapshot. Enforce privacy projection between group and direct-message scopes, admit only whitelisted DMs, and let users remove their retained memory with /forget.
Forget reset only lanes where the user had posted, so sibling threads whose context embedded shared chat memory kept serving forgotten facts after confirmation. Its busy check also read activeRunId before acquiring the lane locks, letting a claim slip past it; the drain-side discard gates could not fully close that window on their own. - Derive the reset lane set from affected namespace scopes and re-read activeRunId under each held lock - Remove the now-unreachable discard gates from the drain flow - Rebase stale build attempts onto the current parent revision instead of superseding them behind their unique watermark - Keep frozen revisions across a forget reset; rendering already skips namespaces with unprocessed forget observations - Single-source FrozenMemoryRevision in run artifacts and reuse the generated MemoryVisibility enum in zod schemas Regression-tested against a throwaway Postgres: both new tests fail on the prior behavior and pass here.
JPEG encoding turned transparent pixels outside the rounded card black. Render the card over its theme background so shared light cards keep clean corners.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.