github / github/copilot-cli

Steering during an in-flight skill-context turn can reuse turn_index and overwrite the persisted user message

オープン
#4,792 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

triage
主要言語
Shell
スター
11.2k
フォーク
1.9k
平均マージ
14時間 16分
マージ済み PR(30日)
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

  1. Human input A receives index 536.
  2. Skill-context message 1 receives 537.
  3. Skill-context message 2 receives 538.
  4. Model-usage events for skill context 2 are recorded with 538.
  5. Before the pending turn is flushed, steering input B arrives with a new interactionId.
  6. B incorrectly also receives 538.
  7. Later B usage is also recorded with 538.
  8. The next human input C receives 539.
  9. The final turns row 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-526
    • initial_tracking_state and initial_tracking_state_result seed turn_counter from persisted max_turn_index + 1.
  • src/runtime/src/session/store_tracking.rs:544-554
    • event_operations flushes the previous pending turn and stores the new user.message in pending_user_message.
  • src/runtime/src/session/store_tracking.rs:869-894
    • flush_pending_turn writes the pending message at the current turn_counter, then increments the counter.
  • src/runtime/src/session_bindings/api_store_tracking.rs:61-78
    • session_store_tracking_init_session_state replaces session.store_tracking_state.
  • src/runtime/src/session_bindings/api_store.rs:295-303
    • assistant.usage reads the mutable tracking_turn_counter and writes an assistant_usage_events row with that index.
  • src/runtime/src/session_bindings/api_store.rs:1033-1047
    • tracking_turn_counter reads the counter from the current session tracking state.
  • src/runtime/src/session/store.rs:971-1002
    • insert_turn_conn uses ON CONFLICT(session_id, turn_index) DO UPDATE, with user_message = COALESCE(excluded.user_message, user_message).
  • src/runtime/src/session/store.rs:1244-1307
    • insert_assistant_usage_event_conn appends usage rows without a uniqueness constraint on (session_id, turn_index).
  • src/cli/core/localSessionManager.ts:256-283
    • initSessionStoreTracking has a waitFor gate intended to flush a previous tracker before resetting shared tracking state and reading getMaxTurnIndex.
  • src/runtime/src/skills/core.rs:2484-2594
    • format_skill_content constructs the injected <skill-context> message.
  • src/runtime/src/tools/session_tool_invoker.rs
    • Tool results carry injected follow-up content through newMessages.

Relevant exported symbols:

  • sessionStoreTrackingInitSessionState
  • SessionStoreHandle.getMaxTurnIndex
  • SessionStoreHandle.handleTrackingEventForSession
  • SessionStoreHandle.flushTrackingForSession
  • SessionStoreHandle.insertTurn
  • SessionStoreHandle.insertAssistantUsageEvent

Likely race

  1. The original tracker has index 538 only in its in-memory pending_user_message.
  2. A steering/session lifecycle path initializes or replaces another tracker state.
  3. It seeds its counter from persisted MAX(turn_index)=537.
  4. It cannot see the other tracker's pending index 538.
  5. 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

  1. Seed the store so the next index is 536.
  2. Submit human A.
  3. Return two skill-context newMessages.
  4. Pause after usage is emitted for the second skill context but before tracker flush.
  5. Submit immediate steering B through the real steering admission path with a new interactionId.
  6. Emit B usage, release the paused turn, flush, and submit C.
  7. Assert the expected indexes and usage assignments above.
  8. Assert that insertTurn is never called with two unequal, non-null user_message values 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_id and a model/request identifier so consumers do not depend exclusively on turn_index.

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. 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 のライフサイクルを再現し、要求されたインデックス、メッセージ保持、使用量割り当てのアサーションに対するカバレッジを追加します。重複したインデックスや、競合するメッセージのサイレントな上書きなしに回帰テストが通れば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
rust, sql, sqlite, typescript
領域
cli, database, testing
issue の種類
バグ
難易度
5/5
見積もり時間
1週間以上
活発さ
活発
明瞭さ
おおむね明確
初心者へのやさしさ
35/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。