Steering during an in-flight skill-context turn can reuse turn_index and overwrite the persisted user message
還沒有人認領這個 Issue。
- 主要語言
- Shell
- 星號
- 11.2k
- 分支
- 1.9k
- 平均合併
- 14 小時 16 分鐘
- 30 天內合併 PR
- 6
描述
Environment
- Copilot CLI 1.0.83-4
- Package build commit:
c05c8e5 - Native session-store tracking implementation
Summary
A steering user.message delivered while an injected skill-context turn is pending can receive the same turn_index as that skill-context message. The conflicting SQLite upsert replaces the earlier skill-context text, while append-only usage events from both interactions retain the shared index.
turn_index is intended to be monotonically increasing and unique for each persisted user.message. This is not expected steering behavior.
Observed sequence
- Human input A receives index 536.
- Skill-context message 1 receives 537.
- Skill-context message 2 receives 538.
- Model-usage events for skill context 2 are recorded with 538.
- Before the pending turn is flushed, steering input B arrives with a new
interactionId. - B incorrectly also receives 538.
- Later B usage is also recorded with 538.
- The next human input C receives 539.
- The final
turnsrow for 538 contains B's text; skill-context message 2 has been overwritten.
Expected
turns
turn_index |
Message |
|---|---|
| 536 | A |
| 537 | Skill context 1 |
| 538 | Skill context 2 |
| 539 | B |
| 540 | C |
Usage
- Pre-B usage: 538
- Post-B usage: 539
Actual
turns
turn_index |
Message |
|---|---|
| 536 | A |
| 537 | Skill context 1 |
| 538 | B |
| 539 | C |
Usage
- Pre-B usage: 538
- Post-B usage: 538
Relevant implementation
Current main locations:
src/runtime/src/session/store_tracking.rs:510-526initial_tracking_stateandinitial_tracking_state_resultseedturn_counterfrom persistedmax_turn_index + 1.
src/runtime/src/session/store_tracking.rs:544-554event_operationsflushes the previous pending turn and stores the newuser.messageinpending_user_message.
src/runtime/src/session/store_tracking.rs:869-894flush_pending_turnwrites the pending message at the currentturn_counter, then increments the counter.
src/runtime/src/session_bindings/api_store_tracking.rs:61-78session_store_tracking_init_session_statereplacessession.store_tracking_state.
src/runtime/src/session_bindings/api_store.rs:295-303assistant.usagereads the mutabletracking_turn_counterand writes anassistant_usage_eventsrow with that index.
src/runtime/src/session_bindings/api_store.rs:1033-1047tracking_turn_counterreads the counter from the current session tracking state.
src/runtime/src/session/store.rs:971-1002insert_turn_connusesON CONFLICT(session_id, turn_index) DO UPDATE, withuser_message = COALESCE(excluded.user_message, user_message).
src/runtime/src/session/store.rs:1244-1307insert_assistant_usage_event_connappends usage rows without a uniqueness constraint on(session_id, turn_index).
src/cli/core/localSessionManager.ts:256-283initSessionStoreTrackinghas awaitForgate intended to flush a previous tracker before resetting shared tracking state and readinggetMaxTurnIndex.
src/runtime/src/skills/core.rs:2484-2594format_skill_contentconstructs the injected<skill-context>message.
src/runtime/src/tools/session_tool_invoker.rs- Tool results carry injected follow-up content through
newMessages.
- Tool results carry injected follow-up content through
Relevant exported symbols:
sessionStoreTrackingInitSessionStateSessionStoreHandle.getMaxTurnIndexSessionStoreHandle.handleTrackingEventForSessionSessionStoreHandle.flushTrackingForSessionSessionStoreHandle.insertTurnSessionStoreHandle.insertAssistantUsageEvent
Likely race
- The original tracker has index 538 only in its in-memory
pending_user_message. - A steering/session lifecycle path initializes or replaces another tracker state.
- It seeds its counter from persisted
MAX(turn_index)=537. - It cannot see the other tracker's pending index 538.
- It independently allocates 538 to B.
The low-level serialized handler does not reproduce the collision when exercised directly. The bug appears to require the integrated steering/session lifecycle path and overlapping tracker initialization or replacement.
Current main already attempts to gate tracker replacement with waitFor, so the reproducing path may be bypassing that gate or creating an additional lifecycle transition not covered by it.
Destructive upsert
insertTurn uses:
ON CONFLICT(session_id, turn_index) DO UPDATE SET
user_message = COALESCE(excluded.user_message, user_message),
assistant_response = COALESCE(excluded.assistant_response, assistant_response)
That behavior is appropriate for combining the user and assistant halves of one turn, but silently corrupts data when two different non-null user messages collide.
Impact
- Loss of a persisted user message.
- Usage generated before B existed appears associated with B.
- No reliable usage-to-interaction correlation remains because usage rows do not persist
interactionId. - Consumers receive corrupted per-turn AIU, token, latency, and model-loop distributions.
- Winsorization and percentile metrics can change even when aggregate AIU is unchanged.
- If the colliding interactions begin under different experiment assignments, A/B arm attribution can also be wrong.
- Overwritten skill-context markers can prevent consumers from recognizing and excluding skill-loading usage.
Missing regression coverage
- Two distinct non-null user messages colliding at one index.
- Multiple skill injections followed by immediate steering.
- Tracker initialization while another tracker has
pending_user_message. - Preservation of usage correlation after an upsert conflict.
Requested regression test
- Seed the store so the next index is 536.
- Submit human A.
- Return two skill-context
newMessages. - Pause after usage is emitted for the second skill context but before tracker flush.
- Submit immediate steering B through the real steering admission path with a new
interactionId. - Emit B usage, release the paused turn, flush, and submit C.
- Assert the expected indexes and usage assignments above.
- Assert that
insertTurnis never called with two unequal, non-nulluser_messagevalues for one(session_id, turn_index).
Acceptance criteria
- Index allocation is serialized or atomically reserved across tracker lifecycle transitions.
- A pending in-memory index cannot be duplicated by a tracker initialized from persisted
MAX(turn_index). - Conflicting non-null user-message upserts are rejected or surfaced rather than silently overwriting data.
- The integrated steering-plus-skill regression test passes.
- Ideally, usage events also persist
interaction_idand a model/request identifier so consumers do not depend exclusively onturn_index.
貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
研究方向
首先追蹤 src/runtime/src/session/store_tracking.rs 和 src/cli/core/localSessionManager.ts 中的 tracker 替換與待處理訊息處理,接著檢查 src/runtime/src/session/store.rs 和 api_store.rs 中的 insertTurn 以及使用量持久化。重現 issue 中描述的整合 steering-plus-skill 生命週期,並為所要求的索引、訊息保留和使用量指派 assertion 新增 coverage。完成的標準是回歸測試通過,且不存在重複索引或對衝突訊息的靜默覆寫。
由索引模型根據 Issue 內容生成。
評估
- 技術堆疊
- rust, sql, sqlite, typescript
- 領域
- cli, database, testing
- Issue 類型
- 缺陷
- 難度
- 5/5
- 預估耗時
- 一週以上
- 活躍度
- 活躍
- 描述清晰度
- 基本清楚
- 新手友好度
- 35/100