SessionMetadataSnapshot.AlreadyInUse is always false for SDK-resumed sessions (never reflects concurrent use by another process)
- 主要语言
- Java
- 星标
- 10.5k
- 派生
- 1.5k
- 平均合并
- 1 天 11 小时
- 30 天内合并 PR
- 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`.
贡献指南
调研方向
先从 dotnet/test/E2E/RpcSessionStateE2ETests.cs 中描述的 resume 和 snapshot 行为开始,然后比较对应的 Go 和 Node.js 端到端测试。跟踪 SDK 的 resume 处理以及 session.metadata.snapshot 响应,以找出检测到的锁定状态在哪里丢失。完成标准是:由一个客户端持有的 session 被另一个客户端 resume 后报告 AlreadyInUse=true,而新 session 仍为 false。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- csharp, go, typescript
- 领域
- api, backend, testing
- Issue 类型
- 缺陷
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 活跃度
- 冷清
- 描述清晰度
- 基本清楚
- 新手友好度
- 48/100