Repository navigation
Quick win: eliminate full-history temporal-ID N+1 work before upstream #1712
Description
Parent investigation: #1708
Architectural follow-up: #1710
Streaming ingress follow-up: #1711
Why this is the first performance task
A real resumed OpenCode session reached the turn: log with 5,580 messages, then spent roughly 74 seconds before ltm-budget; gradient/compression took only about 1.8 seconds afterwards.
The normal-turn path builds the full Lore transcript and calls:
resolveToolResults(loreMessages, (message) =>
temporal.storedMessageId({
projectPath,
sessionID: message.info.sessionID,
sourceID: message.info.id,
legacySourceID: message.legacySourceID,
}),
);resolveToolResults() invokes that callback for user messages reduced to a tool-result placeholder. temporal.storedMessageId() calls resolveMessageId(), which performs a synchronous SQLite compatibility lookup. A long, tool-heavy coding transcript can therefore issue one blocking query for many historical messages before the upstream request begins.
This is narrower, lower risk, and faster to validate than implementing #1710's durable frontier/checkpoint. It is also useful even if profiling shows another stage shares the cost.
Goals
- Make the suspected N+1 visible with precise timings and counts.
- Replace historical per-message storage-ID queries with bounded/batched work.
- Remove avoidable duplicate full-history conversion/resolution after the upstream response.
- Preserve byte/semantic behaviour exactly; this issue does not introduce incremental-frontier semantics.
Phase 1 — instrumentation and representative benchmark
Add a committed 5,580-message, tool-heavy OpenAI Responses/Codex benchmark fixture, sanitised or synthetically generated to preserve the observed structure.
Instrument the pre-upstream stages:
gatewayMessagesToLorewall time and CPU delta;- provenance-map construction;
resolveToolResultstotal time;- number and total duration of
storedMessageIdresolutions; - number of messages, parts, tool uses, tool results and placeholder-producing messages;
- event-loop delay during semantic preparation;
- total
turn:→ upstream-start duration.
Emit Sentry spans/metrics with cardinality-safe dimensions. Do not include transcript content, message IDs, paths, credentials, or raw queries.
The benchmark must report stage breakdown before and after the optimisation. Instrumentation should land with the fix or in a tiny prerequisite PR; avoid leaving an observability-only change as the end state.
Phase 2 — batch storage-ID resolution
Add a bulk API along the lines of:
temporal.storedMessageIds({
projectPath,
sessionID,
messages: Array<{ sourceID: string; legacySourceID?: string }>,
}): Map<string, string>Implementation requirements:
- resolve the project once;
- derive the deterministic v82 IDs in memory;
- fetch existing legacy/current mappings in chunked set-based queries below SQLite's parameter limit;
- return the derived ID where no compatibility row exists;
- preserve tenant/project/session ownership constraints;
- preserve ambiguity/legacy precedence exactly as
resolveMessageId()does; - avoid one
ensureProject()and one SQL statement per message; - use an O(1) lookup map inside
resolveToolResults' placeholder callback.
Optimise further if safe: current-format rows should not require a compatibility query forever. Consider a one-time migration/marker proving that pre-v82 identities no longer exist for a session/database, then use pure deterministic derivation. Do not make that larger migration a blocker for the batched fix.
Phase 3 — stop duplicate post-response full-history processing
The request path has already built and resolved loreMessages before gradient. Post-response temporal storage ultimately needs only:
- the latest source user message in its original tool-result form;
- enough state to resolve/update the preceding tool call;
- the new assistant response.
Refactor so post-response bookkeeping does not call gatewayMessagesToLore(req.messages, ...) and resolveToolResults(...) over the complete historical request again.
Correctness options, in preferred order:
- Capture a narrow immutable
TurnTemporalInputbefore the pre-upstream transform mutates anything. - Pass/reuse the already-converted representation only if ownership and mutation semantics are explicit.
- Reconvert a small boundary suffix, never all 5,580 messages.
Do not retain the complete request graph longer merely to save CPU; measure RSS/heap lifetime as well as latency.
Correctness tests
- Existing pre-v82 temporal rows resolve to their existing storage IDs.
- New v82 rows use the same derived IDs as today.
- Tenant, project and session boundaries cannot cross-resolve.
- Tool-result-only user messages receive the same recall placeholder IDs.
- Parallel tool calls/results and error results remain identical.
- No-store/amnesia path remains read-only and does not create projects.
- Recall markers and OpenAI Responses provenance remain unchanged.
- A post-response failure remains transactionally/idempotently recoverable.
- Query count is bounded by batch chunks, not historical tool-result count.
Performance acceptance criteria
On the representative 5,580-message fixture:
- pre-upstream SQL query count attributable to temporal-ID resolution is
O(chunks)rather thanO(tool-result messages); - duplicate post-response full-history conversion is eliminated;
- stage timings demonstrate a material improvement in
turn:→ upstream-start latency; - no meaningful regression on ordinary short sessions;
- if the change does not materially improve the observed path, preserve the stage instrumentation and use it to identify the next dominant stage before beginning Make long-session resume O(delta): persist a source transcript frontier and active model window #1710.
Non-goals
- Persisting a source transcript frontier or active model-window checkpoint — Make long-session resume O(delta): persist a source transcript frontier and active model window #1710.
- Streaming JSON parsing/decompression — Stream and selectively parse large compressed LLM request bodies #1711.
- Redesigning worker scheduling or embedding backpressure.
- Changing compression or recall semantics.
Activity
fix(perf): batch temporal ID lookups and eliminate duplicate history …