SessionMetadataSnapshot.AlreadyInUse is always false for SDK-resumed sessions (never reflects concurrent use by another process)
- 主要言語
- Java
- スター
- 10.5k
- フォーク
- 1.5k
- 平均マージ
- 1日 11時間
- マージ済み PR(30日)
- 128
説明
## Summary
`session.Rpc.Metadata.SnapshotAsync()` returns a `SessionMetadataSnapshot` whose `AlreadyInUse` is **always `false`** when a session is opened through the SDK — even when the same session is concurrently held open by a different live process. The documented contract says this field should be `true` "when the session was detected to be in use by another process."
The in-use information clearly exists at the time of the call (a live per-process lock file is present on disk in the session directory), but it is not reflected in the snapshot. This affects both a default resume and a resume with `SuppressResumeEvent = true`.
## Environment
- SDK: `GitHub.Copilot.SDK` (.NET) **v1.0.1**
- Bundled Copilot CLI: **1.0.63** (managed automatically by the SDK)
- OS: Windows
- Note: the root cause appears to be server-side (Copilot CLI), since the .NET client deserializes `alreadyInUse` straight from the `session.metadata.snapshot` RPC response. The same field exists in every language SDK (`alreadyInUse` / `AlreadyInUse`), so all clients are likely affected.
## Expected behavior
Per the generated contract for `SessionMetadataSnapshot.AlreadyInUse`:
> True when the session was detected to be in use by another process at construction time. Local consumers may surface a confirmation prompt before fully attaching. Always false for new sessions.
So when a session is resumed while another live process is holding it, `AlreadyInUse` should be `true`.
## Actual behavior
`AlreadyInUse` is `false` in that scenario.
## Minimal reproduction
Self-contained: two `CopilotClient` instances in one program (each spawns its own CLI server process, so the second one is genuinely "another process" relative to the first).
```csharp
using GitHub.Copilot;
#pragma warning disable GHCP001 // SnapshotAsync is experimental
// Client A creates a session, sends one message so it persists to disk,
// then stays alive (its CLI server keeps holding the session).
await using var holder = new CopilotClient();
var held = await holder.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
});
var sessionId = held.SessionId;
await held.SendAndWaitAsync(new MessageOptions { Prompt = "Reply with exactly: OK" });
// Client B (independent client + CLI server) resumes the SAME session
// using the DEFAULT resume path, then reads the snapshot.
await using var probe = new CopilotClient();
await using var resumed = await probe.ResumeSessionAsync(sessionId, new ResumeSessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
});
var snapshot = await resumed.Rpc.Metadata.SnapshotAsync();
Console.WriteLine($"AlreadyInUse = {snapshot.AlreadyInUse}"); // prints False; expected True
```
### Observed output
```
[holder] Created session: f5fdd147-8928-4759-b74c-fd3ffc2b469b
[disk] events.jsonl persisted: True (27817 bytes)
[disk] Lock files present (holder still alive): inuse.20772.lock
[probe] snapshot.AlreadyInUse = False (EXPECTED: True — held by [holder] process)
```
While `probe` calls `SnapshotAsync()`, the session directory
`~/.copilot/session-state/f5fdd147-.../` contains `inuse.20772.lock`, and PID `20772`
(the holder's CLI server) is alive — i.e. the session is demonstrably in use by another
process, yet the snapshot reports `AlreadyInUse = False`.
## Supporting evidence
- The session directory contains a per-process lock file named `inuse..lock` whose contents are the owning process PID. During the repro this file exists and its PID is a live process, so the "in use by another process" condition is objectively true at snapshot time.
- The behavior is identical with `SuppressResumeEvent = true` and with the default resume, so it is not a side effect of suppressing the resume event.
## Hypothesized root cause
On the SDK resume path, the runtime does not surface the in-use/lock-detection result onto the metadata snapshot — `AlreadyInUse` is left at its default (`false`). The underlying data is available (the lock files are on disk and detection runs), so this looks like the computed value simply isn't propagated to the snapshot for SDK-initiated resumes, whereas a brand-new session correctly reports `false`.
## Test coverage gap / suggested fix
The existing e2e tests assert `AlreadyInUse == false` only for a freshly-created session, which is correct per the "Always false for new sessions" clause, but none cover a session that is concurrently in use:
- `dotnet/test/E2E/RpcSessionStateE2ETests.cs` → `Assert.False(initialSnapshot.AlreadyInUse);`
- `go/internal/e2e/rpc_session_state_e2e_test.go`
- `nodejs/test/e2e/rpc_session_state.e2e.test.ts` → `expect(initialSnapshot.alreadyInUse).toBe(false);`
Suggested additions:
1. Populate `alreadyInUse` on the snapshot from the same in-use detection used elsewhere, on the SDK resume path.
2. Add an e2e test: create + hold a session in one client, resume it from a second client, and assert the second client's snapshot reports `AlreadyInUse == true`.
コントリビューションガイド
調査の方向性
Start with the resume and snapshot behavior described in dotnet/test/E2E/RpcSessionStateE2ETests.cs, then compare the corresponding Go and Node.js end-to-end tests. Trace SDK resume handling and the session.metadata.snapshot response to find where the detected lock state is lost. Done means a held session resumed by another client reports AlreadyInUse=true, while new sessions remain false.
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- csharp, go, typescript
- 領域
- api, backend, testing
- issue の種類
- バグ
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 活発さ
- 静か
- 明瞭さ
- おおむね明確
- 初心者へのやさしさ
- 48/100