Sticky root keys keyed only on the first user message caused Claude Code
"New session" + "hi" to reuse a confirmed prior Notion thread (forking history).
Prefer exact conversation-prefix match for multi-turn; keep sticky root for
UREW multi-turn and failed-first-request retries; mint createThread:true when
the sticky root is already confirmed and the request has no assistant history.
* test: hermetic TLS mock for notion thread-session suite + drop duplicated usage-analytics file
Root cause A (#8159): sendNotionInferenceRequest() in
open-sse/executors/notion-web.ts was migrated from fetch() to
tlsFetchNotion() (open-sse/services/notionTlsClient.ts, native
tls-client-node binary) to get past Notion's Cloudflare TLS
fingerprinting. #8159 updated the mock in the sibling
tests/unit/executor-notion-web.test.ts (installNotionTlsMock, wired
through __setTlsFetchOverrideForTesting) but never touched
tests/unit/executor-notion-web-thread-sessions.test.ts (split out
earlier by #7900) — its 3 execute()-driven tests still mocked
globalThis.fetch, which tlsFetchNotion() never calls once the native
TLS client loads successfully. Confirmed live: all 3 tests hit real
https://app.notion.com with a fake cookie and got a real 401
(~8.1-8.5s each here; on a network with blocked/slow egress this
would instead hang up to the client's ~190s timeout+grace per test —
a CI-hang risk).
Fix: replicate installNotionTlsMock verbatim from the sibling file
into executor-notion-web-thread-sessions.test.ts so the 3 tests mock
the TLS override point instead of global fetch. Suite is now fully
hermetic — 8/8 pass, no network I/O, total runtime 31.0s -> 8.0s.
Root cause B (#7700): tests/unit/usage-analytics-route-extra.test.ts
was created as a byte-for-byte duplicate of 10 of the 22 tests in
tests/unit/usage-analytics-route.test.ts. #7300 later fixed a fixture
bug in the retention-window boundary test ("does not double-count raw
and aggregated rows") in the main file only — reading
getUserDatabaseSettings().retention.usageHistory live instead of a
hardcoded 30-day cutoff (default retention is 365 days) — leaving the
duplicate copy on the stale hardcoded value, which now fails (1 !== 2).
Fix: delete the duplicate file. All 10 of its test names exist
verbatim in the main file (verified with comm -12) and that file
passes 22/22:
- does not double-count raw and aggregated rows
- does not persist guessed API key attribution
- does not throw Unknown named parameter on short range (needsAggregated=false)
- does not throw Unknown named parameter with apiKey filter on long range
- groups renamed API key usage by stable ID
- includes activityMap for heatmap
- includes cost by API key
- omits global aggregates when filtering by API key
- returns 500 on database errors
- returns weeklyPattern for the costs dashboard
No coverage loss — same production code, same assertions, one fewer
redundant file.
Validation:
- RED executor-notion-web-thread-sessions.test.ts: 5 pass / 3 fail
(401 !== 200, real network hit), 31.0s
- RED usage-analytics-route-extra.test.ts: 9 pass / 1 fail
(1 !== 2), 18.7s
- GREEN executor-notion-web-thread-sessions.test.ts: 8/8 pass, 8.0s,
hermetic (no network)
- GREEN executor-notion-web.test.ts (sibling, untouched): 37/37
pass, byte-identical diff
- GREEN usage-analytics-route.test.ts (untouched): 22/22 pass,
byte-identical diff
- npx eslint on the changed file: clean
- npm run typecheck:core: clean (exit 0)
Refs #8159
Refs #7300
Refs #7700
* chore(quality): register usage-analytics-route-extra deletion in test-masking allowlist
check:test-masking hard-flags any deleted test file without a
_deletedWithReplacement entry. The deletion is legitimate (100% duplicate
suite, coverage retained verbatim in tests/unit/usage-analytics-route.test.ts)
-- same registration pattern as the video-dashscope entry.
Refs #7700
Refs #7300
* fix(notion-web): reuse threadId across OpenAI multi-turn (no new chat each request)
Root cause: every execute() minted a random threadId with createThread:true,
so each OpenAI messages[] turn became a brand-new Notion AI chat. That broke
multi-turn agent flows (tool result follow-ups looked like cold starts).
- History-keyed in-memory session cache (spaceId + conversation prefix hash)
- First user turn: createThread true + new UUID
- Follow-up with prior turns: createThread false + same threadId
- Optional client continuity: body.notion_thread_id / X-Notion-Thread-Id
- Echo thread id on chat.completion (notion_thread_id + response header)
- Also accept OpenAI content-parts arrays for message content
- Unit tests: 34/34 (session lookup/store + createThread false on turn 2)
* fix(notion-web): read X-Notion-Thread-Id from clientHeaders
ExecuteInput exposes client request headers as clientHeaders, not headers.
input.headers was always undefined so client-supplied thread pins were ignored.
* fix(notion-web): prefer clientHeaders with defensive headers fallback
* fix(notion-web): sticky threads on errors + partial follow-ups
- Bind conversation root (first user) to a threadId *before* upstream call so
temporarily-unavailable / empty replies never mint a new Notion chat on retry
- Persist sticky map under DATA_DIR so multi-turn survives process restarts
- Follow-ups use createThread:false, isPartialTranscript:true, and only the
steps after the last assistant (full re-transcript was overloading Notion)
- Detect in-band Notion error objects (subType temporarily-unavailable) and
retry once with the same threadId
- Keep custom-agent workflowId support and clientHeaders thread pin
* refactor(notion-web): split thread-session/stream-parser/transcript-builder into services
The merged notion-web.ts (1490 lines) and its test file (1000 lines) tripped
the file-size gate (cap 800 for new/uncapped files). Extract three
self-contained pieces into open-sse/services/, no behavior change:
- notionThreadSessions.ts: sticky thread-session cache, disk persistence,
conversation hashing, client thread-id pin (body/header)
- notionStreamParser.ts: NDJSON runInferenceTranscript response parsing +
in-band upstream error detection
- notionTranscriptBuilder.ts: config/context/message-step transcript building
Split the corresponding "Notion thread session continuity" describe block
into tests/unit/executor-notion-web-thread-sessions.test.ts. All symbols
previously reachable via the notion-web.ts namespace import stay reachable
(re-exported) so existing test destructuring is unaffected. 44/44 tests pass.
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Artur <artur@local>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>