openArtifactPath/_workspacePathExists split on '/' but Windows artifact
paths arrive as backslash absolute paths. Regression tests drive the real
functions from static/workspace.js via node (mirroring
test_issue3262_artifact_path_canonicalization.py): backslash subdir and
root artifacts must open, POSIX relative clicks must keep working, and a
genuinely missing artifact must still surface file_open_failed.
- _handle_memory_read: pass the request-owned get_config_snapshot() into
_external_notes_sources_enabled() so no ambient/process-global config is
re-read later in the handler (review requirement).
- tests: add cross-profile forced-reload regressions for both directions
(A disabled/B enabled and inverse), covering memory_enabled and
user_profile_enabled for read (no content/path/mtime) and write (403 +
byte-identical file).
- tests: adapt _external_notes_sources_enabled mock arity in
test_3866_project_context_memory_section.py to the new snapshot param.
Review round 2: the 1.5s timestamp tolerance over-deduped a legitimate
rapid repeat — two identical-text turns <1.5s apart collapsed into one,
hiding the second turn and copying its attachments onto the earlier row
(the mirror risk of #6649).
_pendingActiveTurnUserMessage now matches only on unambiguous identity:
- the row carries the server-stamped _active_turn_token (stream_id +
started_at, per build_active_turn_token), or
- its timestamp equals pending_started_at within a precision-only
epsilon (1e-6, absorbs float/state.db drift, never a full second).
Anything wider (whole-second truncation, ~1s rapid repeat) returns null
so getPendingSessionMessage() materializes the pending turn — fail
toward the harmless transient duplicate the settle render clears.
Regressions added: two completed identical-text turns ~1s apart → second
pending row returned + first row's attachments untouched; token-identity
row adopted even when its timestamp is outside the epsilon.
Per review feedback:
- Frontend: assert exact fetch(mediaUrl, {cache:'no-store'}) in
loadHtmlInline body, not just 'fetch(mediaUrl' prefix (too broad,
other fetch calls match the prefix)
- Backend: assert Cache-Control: no-store on both attachment and inline
HTML responses in test_html_media_endpoint_inline_requires_csp_sandbox
- Both assertions fail independently if either the frontend no-store
option or backend no-store policy is reverted
Claude Code sessions listed in the sidebar rendered "Session not available
in web UI." when clicked under a named (non-root) profile.
Root cause: get_claude_code_sessions() scans ~/.claude/projects and stamps
profile=None on every row, because those JSONL transcripts belong to no
Hermes profile. /api/sessions lists them regardless of active profile, but
the GET /api/session detail load ran them through
_session_visible_to_active_profile, whose _profiles_match coerces None ->
'default'. With hermes_profile=feng-family active, the gate 404'd before
_claim_or_synthesize_cli_session ever ran, so the frontend hit its 404
branch and painted the empty-state message. POST /api/session/import_cli
correctly returned 200 with an inline read-only payload (Claude Code
sessions are read_only by design and never get a sidecar), which is why no
file appeared in webui/sessions/ -- Session.save() was never supposed to
run and was not silently failing.
Exempt profile-less claude_code rows from the detail-load profile gate via
_is_profile_agnostic_foreign_session(). Profile-tagged foreign rows keep
the #5419 409 cross-profile contract.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
While a turn is live the server can reconcile that turn's user row into the
transcript returned by /api/session (the sidecar/state.db merge picks it up
from state.db, where the agent writes it immediately) while
pending_user_message is still set. The row is then followed by the same
turn's assistant/tool rows.
_pendingCurrentTailUserMessage() scans the tail backwards and returns null as
soon as it meets a completed assistant, so in that state it reports "the
current turn has no user row". getPendingSessionMessage() then materializes
pending_user_message a second time and the pane renders two identical user
bubbles until the settle render replaces the list wholesale (which is why the
duplicate disappears when the answer lands, and why nothing wrong is ever
persisted).
Scanning past assistant/tool rows on text alone would regress #3300: a user
who submits the same text twice in a row legitimately gets two identical user
turns. Use pending_started_at as the discriminator instead — the server stamps
the active turn's user row with it, and no earlier turn can share it. Text
equality is still required, so a false match needs identical text AND a
near-identical timestamp. Absent/invalid pending_started_at disables the
fallback and keeps the previous strict-tail behaviour.
Verified: the new midRunReloadDedupe assertion fails on the unpatched ui.js
and passes with the fix; the existing repeat-prompt, compaction-boundary and
strict-tail assertions still pass.
Move the staged agent source from /tmp/hermes-agent-build (ephemeral
tmpfs) to /app/hermes-agent-src (persistent, same lifecycle as the
venv). The editable install records this path in a .pth file - placing
it alongside the venv ensures both survive container restarts and are
lost together on recreation, eliminating the dangling-link class of
bugs without sentinel files or state machines.
Three states now handled by lifecycle equivalence:
- First boot: stages source, installs editable, writes .deps_installed
- Restart (same container): .deps_installed + staged source both present
- Recreation (new container): both gone, fresh boot re-stages
Clarify that preflight compression is intentionally excluded because
the later authoritative 'Compacting context' marker from
conversation_compression is the real signal that compression
proceeded. The preflight status can fire even when compression
exits before compaction (e.g. durable guard refresh, Codex-Hermes
mode with no active thread).
PR #6184 narrowed the agent-status-to-SSE bridge to real compression
start notices, but the 'preflight compression:' pattern matches the
Hermes agent's preflight check message ("Preflight compression: ~X
tokens >= Y threshold"). This fires on every new session where the
system prompt alone exceeds the threshold, showing a "Compressing
context" card before any model output — a false positive.
Remove 'preflight compression:' from the pattern. The preflight check
is a one-time pre-model-call estimate, not a mid-conversation
compression event that the user needs to see.