Skip to content

Commit 7287bae

Browse files
Adapt hand-written SDK code to CLI 1.0.76-2 wire changes
Ports the follow-up fixes from the 1.0.76-0 bump (session.fs SQLite transaction provider APIs, resetSessionApprovals params, Go/.NET shell test teardown, rustfmt import wrapping) and applies the additional adjustments 1.0.76-2 requires: - session.commands.list request renamed to SessionCommandsListRequest - model.switchTo gained deferIfModelChangeQueued - SessionModelSwitchToResult gained deferred - session.history.compact params gained customInstructions/trigger/tokenLimit Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 72193c60-62cd-459b-a6f3-4b2798a7eb4b
1 parent 2a435a9 commit 7287bae

38 files changed

Lines changed: 914 additions & 119 deletions

dotnet/src/Session.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1823,6 +1823,7 @@ await Rpc.Model.SwitchToAsync(
18231823
null,
18241824
options.ModelCapabilities,
18251825
options.ContextTier,
1826+
null,
18261827
cancellationToken);
18271828
}
18281829

dotnet/src/SessionFsProvider.cs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*--------------------------------------------------------------------------------------------*/
44

55
using GitHub.Copilot.Rpc;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.Text.Json;
78

89
namespace GitHub.Copilot;
@@ -27,6 +28,23 @@ public sealed class SessionFsSqliteResult
2728
public long? LastInsertRowid { get; set; }
2829
}
2930

31+
/// <summary>
32+
/// One statement in an atomic SQLite transaction passed to
33+
/// <see cref="ISessionFsSqliteProvider.TransactionAsync"/>.
34+
/// </summary>
35+
[Experimental(Diagnostics.Experimental)]
36+
public sealed class SessionFsSqliteStatement
37+
{
38+
/// <summary>How to execute: <c>"exec"</c>, <c>"query"</c>, or <c>"run"</c>.</summary>
39+
public SessionFsSqliteQueryType QueryType { get; set; }
40+
41+
/// <summary>SQL statement to execute.</summary>
42+
public string Query { get; set; } = string.Empty;
43+
44+
/// <summary>Optional named bind parameters.</summary>
45+
public IDictionary<string, object?>? Params { get; set; }
46+
}
47+
3048
/// <summary>
3149
/// Optional interface for <see cref="SessionFsProvider"/> subclasses that support
3250
/// per-session SQLite databases. Implement this interface on your provider to enable
@@ -48,13 +66,53 @@ public interface ISessionFsSqliteProvider
4866
IDictionary<string, object?>? bindParams,
4967
CancellationToken cancellationToken);
5068

69+
/// <summary>
70+
/// Executes <paramref name="statements"/> atomically against the per-session database.
71+
/// </summary>
72+
/// <param name="statements">Statements to execute in order, inside a single transaction.</param>
73+
/// <param name="cancellationToken">Cancellation token.</param>
74+
/// <returns>One result per statement, in the same order as <paramref name="statements"/>.</returns>
75+
/// <exception cref="SessionFsSqliteTransactionException">
76+
/// Thrown to tell the runtime how the failure should be classified. Any other exception
77+
/// is reported as <see cref="SessionFsSqliteTransactionErrorClass.Fatal"/>.
78+
/// </exception>
79+
Task<IList<SessionFsSqliteResult>> TransactionAsync(
80+
IList<SessionFsSqliteStatement> statements,
81+
CancellationToken cancellationToken);
82+
5183
/// <summary>
5284
/// Checks whether the per-session SQLite database already exists, without creating it.
5385
/// </summary>
5486
/// <param name="cancellationToken">Cancellation token.</param>
5587
Task<bool> ExistsAsync(CancellationToken cancellationToken);
5688
}
5789

90+
/// <summary>
91+
/// Thrown by an <see cref="ISessionFsSqliteProvider"/> to classify a failed SQLite transaction.
92+
/// <see cref="SessionFsSqliteTransactionErrorClass.BusyOrLocked"/> guarantees the transaction
93+
/// rolled back and is safe to retry; <see cref="SessionFsSqliteTransactionErrorClass.PostCommitAmbiguous"/>
94+
/// must never be retried.
95+
/// </summary>
96+
[Experimental(Diagnostics.Experimental)]
97+
public sealed class SessionFsSqliteTransactionException : Exception
98+
{
99+
/// <summary>Initializes a new instance of the <see cref="SessionFsSqliteTransactionException"/> class.</summary>
100+
/// <param name="message">Human-readable failure description.</param>
101+
/// <param name="errorClass">How the runtime should classify the failure.</param>
102+
/// <param name="innerException">Optional underlying exception.</param>
103+
public SessionFsSqliteTransactionException(
104+
string message,
105+
SessionFsSqliteTransactionErrorClass errorClass,
106+
Exception? innerException = null)
107+
: base(message, innerException)
108+
{
109+
ErrorClass = errorClass;
110+
}
111+
112+
/// <summary>Gets the failure classification reported to the runtime.</summary>
113+
public SessionFsSqliteTransactionErrorClass ErrorClass { get; }
114+
}
115+
58116
/// <summary>
59117
/// Base class for session filesystem providers. Subclasses override the
60118
/// virtual methods and use normal C# patterns (return values, throw exceptions).
@@ -309,6 +367,64 @@ async Task<SessionFsSqliteQueryResult> ISessionFsHandler.SqliteQueryAsync(Sessio
309367
}
310368
}
311369

370+
async Task<SessionFsSqliteTransactionResult> ISessionFsHandler.SqliteTransactionAsync(SessionFsSqliteTransactionRequest request, CancellationToken cancellationToken)
371+
{
372+
if (this is not ISessionFsSqliteProvider sqliteProvider)
373+
{
374+
return new SessionFsSqliteTransactionResult
375+
{
376+
Error = new SessionFsSqliteTransactionError
377+
{
378+
ErrorClass = SessionFsSqliteTransactionErrorClass.Fatal,
379+
Message = "SQLite is not supported by this provider.",
380+
},
381+
};
382+
}
383+
384+
IList<SessionFsSqliteResult> results;
385+
try
386+
{
387+
var statements = request.Statements.Select(statement => new SessionFsSqliteStatement
388+
{
389+
QueryType = statement.QueryType,
390+
Query = statement.Query,
391+
Params = statement.Params?.ToDictionary(kvp => kvp.Key, kvp => JsonElementToValue(kvp.Value)),
392+
}).ToList();
393+
results = await sqliteProvider.TransactionAsync(statements, cancellationToken).ConfigureAwait(false);
394+
}
395+
catch (SessionFsSqliteTransactionException ex)
396+
{
397+
return new SessionFsSqliteTransactionResult
398+
{
399+
Error = new SessionFsSqliteTransactionError { ErrorClass = ex.ErrorClass, Message = ex.Message },
400+
};
401+
}
402+
catch (Exception ex)
403+
{
404+
return new SessionFsSqliteTransactionResult
405+
{
406+
Error = new SessionFsSqliteTransactionError
407+
{
408+
ErrorClass = SessionFsSqliteTransactionErrorClass.Fatal,
409+
Message = ex.Message,
410+
},
411+
};
412+
}
413+
414+
return new SessionFsSqliteTransactionResult
415+
{
416+
Results = results.Select(result => new SessionFsSqliteQueryResult
417+
{
418+
Rows = result.Rows?.Select(row => (IDictionary<string, JsonElement>)row.ToDictionary(
419+
kvp => kvp.Key,
420+
kvp => CopilotClient.ToJsonElementForWire(kvp.Value)!.Value)).ToList() ?? [],
421+
Columns = result.Columns ?? [],
422+
RowsAffected = result.RowsAffected,
423+
LastInsertRowid = result.LastInsertRowid,
424+
}).ToList(),
425+
};
426+
}
427+
312428
async Task<SessionFsSqliteExistsResult> ISessionFsHandler.SqliteExistsAsync(SessionFsSqliteExistsRequest request, CancellationToken cancellationToken)
313429
{
314430
if (this is not ISessionFsSqliteProvider sqliteProvider)

dotnet/test/E2E/CommandsE2ETests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public async Task Session_Commands_List_Returns_Builtins_And_Respects_Client_Com
3030
await TestHelper.WaitForConditionAsync(
3131
async () =>
3232
{
33-
clientCommands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
33+
clientCommands = await session.Rpc.Commands.ListAsync(new SessionCommandsListRequest
3434
{
3535
IncludeBuiltins = false,
3636
IncludeClientCommands = true,
@@ -45,7 +45,7 @@ await TestHelper.WaitForConditionAsync(
4545
Assert.Contains(clientCommands.Commands, c => IsCommand(c, "rollback", SlashCommandKind.Client));
4646
Assert.DoesNotContain(clientCommands.Commands, c => c.Kind == SlashCommandKind.Builtin);
4747

48-
var builtinCommands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
48+
var builtinCommands = await session.Rpc.Commands.ListAsync(new SessionCommandsListRequest
4949
{
5050
IncludeBuiltins = true,
5151
IncludeClientCommands = false,
@@ -64,7 +64,7 @@ public async Task Session_Commands_Invoke_Known_Builtin_Returns_Expected_Result(
6464
{
6565
var session = await CreateSessionAsync();
6666

67-
var builtinCommands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
67+
var builtinCommands = await session.Rpc.Commands.ListAsync(new SessionCommandsListRequest
6868
{
6969
IncludeBuiltins = true,
7070
IncludeClientCommands = false,
@@ -128,7 +128,7 @@ public async Task Session_Commands_Execute_Runs_Registered_Command_Handler()
128128
await TestHelper.WaitForConditionAsync(
129129
async () =>
130130
{
131-
var commands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
131+
var commands = await session.Rpc.Commands.ListAsync(new SessionCommandsListRequest
132132
{
133133
IncludeBuiltins = false,
134134
IncludeClientCommands = true,

dotnet/test/E2E/InMemorySessionFsSqliteHandler.cs

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,28 +45,68 @@ private SqliteConnection GetOrCreateDb()
4545
string query,
4646
IDictionary<string, object?>? bindParams,
4747
CancellationToken cancellationToken)
48+
{
49+
return Task.FromResult(RunStatement(GetOrCreateDb(), null, queryType, query, bindParams));
50+
}
51+
52+
public Task<IList<SessionFsSqliteResult>> TransactionAsync(
53+
IList<SessionFsSqliteStatement> statements,
54+
CancellationToken cancellationToken)
55+
{
56+
var db = GetOrCreateDb();
57+
using var transaction = db.BeginTransaction();
58+
try
59+
{
60+
IList<SessionFsSqliteResult> results = statements
61+
.Select(statement => RunStatement(db, transaction, statement.QueryType, statement.Query, statement.Params)
62+
?? new SessionFsSqliteResult())
63+
.ToList();
64+
transaction.Commit();
65+
return Task.FromResult(results);
66+
}
67+
catch (SqliteException ex)
68+
{
69+
transaction.Rollback();
70+
var errorClass = ex.SqliteErrorCode is 5 or 6
71+
? SessionFsSqliteTransactionErrorClass.BusyOrLocked
72+
: SessionFsSqliteTransactionErrorClass.Fatal;
73+
throw new SessionFsSqliteTransactionException(ex.Message, errorClass, ex);
74+
}
75+
catch (Exception ex)
76+
{
77+
transaction.Rollback();
78+
throw new SessionFsSqliteTransactionException(ex.Message, SessionFsSqliteTransactionErrorClass.Fatal, ex);
79+
}
80+
}
81+
82+
private SessionFsSqliteResult? RunStatement(
83+
SqliteConnection db,
84+
SqliteTransaction? transaction,
85+
SessionFsSqliteQueryType queryType,
86+
string query,
87+
IDictionary<string, object?>? bindParams)
4888
{
4989
sqliteCalls.Add(new SqliteCall(sessionId, queryType.Value, query));
5090

5191
var trimmed = query.Trim();
5292
if (trimmed.Length == 0)
5393
{
54-
return Task.FromResult<SessionFsSqliteResult?>(null);
94+
return null;
5595
}
5696

57-
var db = GetOrCreateDb();
58-
5997
if (queryType == SessionFsSqliteQueryType.Exec)
6098
{
6199
using var cmd = db.CreateCommand();
100+
cmd.Transaction = transaction;
62101
cmd.CommandText = trimmed;
63102
cmd.ExecuteNonQuery();
64-
return Task.FromResult<SessionFsSqliteResult?>(null);
103+
return null;
65104
}
66105

67106
if (queryType == SessionFsSqliteQueryType.Query)
68107
{
69108
using var cmd = db.CreateCommand();
109+
cmd.Transaction = transaction;
70110
cmd.CommandText = trimmed;
71111
AddParams(cmd, bindParams);
72112

@@ -88,33 +128,35 @@ private SqliteConnection GetOrCreateDb()
88128
rows.Add(row);
89129
}
90130

91-
return Task.FromResult<SessionFsSqliteResult?>(new SessionFsSqliteResult
131+
return new SessionFsSqliteResult
92132
{
93133
Columns = columns,
94134
Rows = rows,
95135
RowsAffected = 0,
96-
});
136+
};
97137
}
98138

99139
if (queryType == SessionFsSqliteQueryType.Run)
100140
{
101141
using var cmd = db.CreateCommand();
142+
cmd.Transaction = transaction;
102143
cmd.CommandText = trimmed;
103144
AddParams(cmd, bindParams);
104145

105146
var rowsAffected = cmd.ExecuteNonQuery();
106147

107148
using var rowidCmd = db.CreateCommand();
149+
rowidCmd.Transaction = transaction;
108150
rowidCmd.CommandText = "SELECT last_insert_rowid()";
109151
var lastRowid = rowidCmd.ExecuteScalar();
110152

111-
return Task.FromResult<SessionFsSqliteResult?>(new SessionFsSqliteResult
153+
return new SessionFsSqliteResult
112154
{
113155
Columns = [],
114156
Rows = [],
115157
RowsAffected = rowsAffected,
116158
LastInsertRowid = lastRowid is long l ? l : null,
117-
});
159+
};
118160
}
119161

120162
throw new ArgumentException($"Unknown queryType: {queryType}");

dotnet/test/E2E/RpcShellAndFleetE2ETests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public async Task Should_Kill_Shell_Process()
4141
var killResult = await session.Rpc.Shell.KillAsync(execResult.ProcessId);
4242

4343
Assert.True(killResult.Killed);
44+
45+
await session.DisposeAsync();
4446
}
4547

4648
[Fact]

dotnet/test/E2E/SessionFsE2ETests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,9 @@ protected override Task RenameAsync(string src, string dest, CancellationToken c
616616
Task<SessionFsSqliteResult?> ISessionFsSqliteProvider.QueryAsync(SessionFsSqliteQueryType queryType, string query, IDictionary<string, object?>? bindParams, CancellationToken cancellationToken) =>
617617
Task.FromException<SessionFsSqliteResult?>(exception);
618618

619+
Task<IList<SessionFsSqliteResult>> ISessionFsSqliteProvider.TransactionAsync(IList<SessionFsSqliteStatement> statements, CancellationToken cancellationToken) =>
620+
Task.FromException<IList<SessionFsSqliteResult>>(exception);
621+
619622
Task<bool> ISessionFsSqliteProvider.ExistsAsync(CancellationToken cancellationToken) =>
620623
Task.FromException<bool>(exception);
621624
}

go/internal/e2e/commands_and_elicitation_e2e_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func TestCommandsE2E(t *testing.T) {
5353
var clientCommands *rpc.CommandList
5454
waitForRPCCondition(t, 30*time.Second, "client commands to be listed", func() (bool, error) {
5555
var err error
56-
clientCommands, err = session.RPC.Commands.List(t.Context(), &rpc.CommandsListRequest{
56+
clientCommands, err = session.RPC.Commands.List(t.Context(), &rpc.SessionCommandsListRequest{
5757
IncludeBuiltins: rpcPtr(false),
5858
IncludeClientCommands: rpcPtr(true),
5959
IncludeSkills: rpcPtr(false),
@@ -68,7 +68,7 @@ func TestCommandsE2E(t *testing.T) {
6868
t.Fatalf("Expected client-command-only list to exclude builtins, got %+v", clientCommands.Commands)
6969
}
7070

71-
builtinCommands, err := session.RPC.Commands.List(t.Context(), &rpc.CommandsListRequest{
71+
builtinCommands, err := session.RPC.Commands.List(t.Context(), &rpc.SessionCommandsListRequest{
7272
IncludeBuiltins: rpcPtr(true),
7373
IncludeClientCommands: rpcPtr(false),
7474
IncludeSkills: rpcPtr(false),
@@ -93,7 +93,7 @@ func TestCommandsE2E(t *testing.T) {
9393
}
9494
defer session.Disconnect()
9595

96-
builtinCommands, err := session.RPC.Commands.List(t.Context(), &rpc.CommandsListRequest{
96+
builtinCommands, err := session.RPC.Commands.List(t.Context(), &rpc.SessionCommandsListRequest{
9797
IncludeBuiltins: rpcPtr(true),
9898
IncludeClientCommands: rpcPtr(false),
9999
IncludeSkills: rpcPtr(false),
@@ -152,7 +152,7 @@ func TestCommandsE2E(t *testing.T) {
152152
defer session.Disconnect()
153153

154154
waitForRPCCondition(t, 30*time.Second, "registered deploy command", func() (bool, error) {
155-
commands, err := session.RPC.Commands.List(t.Context(), &rpc.CommandsListRequest{
155+
commands, err := session.RPC.Commands.List(t.Context(), &rpc.SessionCommandsListRequest{
156156
IncludeBuiltins: rpcPtr(false),
157157
IncludeClientCommands: rpcPtr(true),
158158
IncludeSkills: rpcPtr(false),

go/internal/e2e/rpc_session_state_e2e_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,7 @@ func TestRPCSessionStateE2E(t *testing.T) {
10831083
t.Errorf("Expected SetApproveAll(true) to succeed, got %+v", approve)
10841084
}
10851085

1086-
reset, err := session.RPC.Permissions.ResetSessionApprovals(t.Context())
1086+
reset, err := session.RPC.Permissions.ResetSessionApprovals(t.Context(), &rpc.PermissionsResetSessionApprovalsRequest{})
10871087
if err != nil {
10881088
t.Fatalf("Failed to call ResetSessionApprovals: %v", err)
10891089
}

go/internal/e2e/rpc_shell_and_fleet_e2e_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ func TestRPCShellAndFleetE2E(t *testing.T) {
8282
if !kill.Killed {
8383
t.Errorf("Expected shell.kill to report Killed=true, got %+v", kill)
8484
}
85+
86+
if err := session.Disconnect(); err != nil {
87+
t.Fatalf("Failed to disconnect session: %v", err)
88+
}
8589
})
8690

8791
t.Run("should start fleet and complete custom tool task", func(t *testing.T) {

0 commit comments

Comments
 (0)