Codex+Opus gate findings on the profile-scoping PR:
1. panels.js: retag S.session.profile on ANY profile switch (was inside the
if(data.default_model) block, so model-less profile switches left a stale chip).
2. sessions.js: project-picker filter now mirrors the server's root-alias
tolerance (default <-> renamed-root) so a server-approved 'default' project
isn't hidden for a renamed-root session.
3. routes.py /api/projects/create: validate the optional client-supplied profile
via _PROFILE_ID_RE before stamping (was trusting raw client input -> could
create hidden cross-profile rows). Updated the PR's string-assertion test.
Supersedes the v0.51.199 proximity-re-pin (#3330) and the #3250 upward-intent
timeout with a sticky-unpin model (ChatGPT/Claude/Codex behavior): scroll up =
stay put until you return to the bottom or click the scroll-to-bottom control.
Reconciled against the shipped #3330 code: removed the now-dead
_recentMessageUpwardIntent reference from the #3319 rAF retry, kept the
load-time -Infinity intent-init fix, kept the pinned-only >500 catch-up.
Co-authored-by: pamnard <pamnard@users.noreply.github.com>
Codex+Opus regression-gate finding: _isRecoveryControlMessage /
_streamRecoveryControlMessage fell back to matching
provider_details_label==='interruption details'. But a GENUINE 'Response
interrupted' card (Stop button, real provider crash) carries that exact label,
so the filter would drop a real user-facing interruption from the transcript on
the next render/restore — the inverse of the #3300 data-loss class. Require the
explicit server-set recovery_control marker; keep only the two fully-anchored
synthetic-text matches for pre-marker backward-compat. Adds a node-driven
regression test (revert-verified) asserting a label-only interruption card and a
real user turn stay visible while marker + strict text are filtered.
#3313 applies the ephemeral-field carry-forward at all 5 wholesale S.messages
replace sites (loadSession/_ensureMessagesLoaded/_loadOlderMessages/
_ensureAllMessagesLoaded/startGatewaySSE), renaming the replace RHS from
msgs/next to _msgsToAssign/_nextToAssign. These pre-existing tests pinned the
old literals; updated to match the new RHS (or made LHS-agnostic) while
preserving the invariants they protect (bump-before-replace, session-switch
guard, shorter-transcript guard).
Codex follow-up finding: _lastMessageUpwardIntentMs/_lastNonMessageScrollIntentMs
initialized to 0, so _recentMessageUpwardIntent() returned true for the first 2s
after load (performance.now() < MESSAGE_UPWARD_INTENT_MS=2000) even with no user
scroll — which would disable the new #3319 retry guard during initial load. Sentinel
-Infinity makes 'no event recorded yet' read as no-intent. Also tightens the
pre-existing scrollIfPinned/settle callers that read the same helpers.
Codex regression-gate finding: the new requestAnimationFrame retry in
_setMessageScrollToBottom re-asserted scrollTop=scrollHeight + _scrollPinned=true
on the next layout frame unconditionally, so a user who scrolled up in that ~16ms
window during streaming would be snapped back and re-pinned, bypassing the
scrollIfPinned early-return guards. Re-check _messageUserUnpinned / upward-intent /
non-message-scroll-intent / !_scrollPinned inside the retry; on hit, only release
the programmatic-scroll latch and bail.
Codex regression-gate finding: since Session.save() no longer auto-clears the
truncation_watermark, the unconditional 'timestamp > watermark' skip in
merge_session_messages_append_only became a permanent ceiling — a genuine future
state.db-only row (recovery/compaction, missed by the sidecar) would be silently
dropped from /api/session and model-context reconstruction forever. Only apply the
above-watermark skip while the sidecar has NOT advanced past the watermark. Preserves
the #2914 deleted-tail filtering (revert-verified). Adds 2 regression tests.
Codex regression-gate finding: the OpenAI tool_calls loop dereferenced
tc.function with no null/type guard, so a persisted message.tool_calls
array containing a null or non-object entry would throw and abort artifact
collection. Mirror the existing tool_use-block guard. Adds a node-driven
regression test.
Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com>
Fixes type annotations where non-optional types (str, int) are used with
None defaults. All affected parameters now use Optional[T] = None.
Closes#3322
Two bugs prevented clicking Artifacts entries from opening files:
1. collectSessionArtifacts() only read S.toolCalls, but
_syncToolCallsForLoadedMessages clears it when messages carry
their own tool_calls/tool_use metadata. Fix: also scan messages'
structured tool data (OpenAI tool_calls array + Anthropic
tool_use content blocks).
2. openArtifactPath() only stripped ~/ and ./ prefixes. When artifact
paths were absolute (e.g. /mnt/.../workspace/file.js), /api/list
received the full absolute path and returned 404. Fix: strip the
session workspace prefix before calling _workspacePathExists.
Bonus: renderSessionArtifacts() now displays workspace-relative paths
instead of cluttered absolute paths in the artifact list.