Skip to content

Commit 734d80d

Browse files
Phase 4g: DateTimeOffset timestamps + PermissionRequestResult.Feedback
- Add UnixMillisecondsDateTimeOffsetConverter for unix-millis-as-JSON-number timestamps - Convert long Timestamp fields on hook *Input* types and PingResponse to DateTimeOffset via the new converter - Convert SessionLifecycleEventMetadata.StartTime/ModifiedTime from ISO 8601 strings to DateTimeOffset - Convert SessionMetadata.StartTime/ModifiedTime from DateTime to DateTimeOffset - Add PermissionRequestResult.Feedback property (parity with the RPC PermissionDecisionReject type's feedback field); forward to the wire only on reject decisions - Tidy XML doc on PermissionRequestResult.Kind to reference the well-known PermissionRequestResultKind static members directly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 23626b9 commit 734d80d

4 files changed

Lines changed: 61 additions & 22 deletions

File tree

dotnet/src/Session.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,10 @@ private async Task ExecutePermissionAndRespondAsync(string requestId, Permission
755755
return;
756756
}
757757
var responseRpcTimestamp = Stopwatch.GetTimestamp();
758-
await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, new PermissionDecision { Kind = result.Kind.Value });
758+
PermissionDecision decision = result.Kind == PermissionRequestResultKind.Rejected
759+
? new PermissionDecisionReject { Feedback = result.Feedback }
760+
: new PermissionDecision { Kind = result.Kind.Value };
761+
await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, decision);
759762
LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null,
760763
"CopilotSession.ExecutePermissionAndRespondAsync response sent successfully. Elapsed={Elapsed}, SessionId={SessionId}, RequestId={RequestId}",
761764
responseRpcTimestamp,

dotnet/src/Types.cs

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -689,13 +689,13 @@ public override void Write(Utf8JsonWriter writer, PermissionRequestResultKind va
689689
public sealed class PermissionRequestResult
690690
{
691691
/// <summary>
692-
/// Permission decision kind. Use the static members of <see cref="PermissionRequestResultKind"/>
693-
/// to construct values. Valid kinds are:
692+
/// Permission decision kind. Construct values with the static members on
693+
/// <see cref="PermissionRequestResultKind"/>:
694694
/// <list type="bullet">
695-
/// <item><description><c>"approve-once"</c> (<see cref="PermissionRequestResultKind.Approved"/>) — allow this single request.</description></item>
696-
/// <item><description><c>"reject"</c> (<see cref="PermissionRequestResultKind.Rejected"/>) — deny the request.</description></item>
697-
/// <item><description><c>"user-not-available"</c> (<see cref="PermissionRequestResultKind.UserNotAvailable"/>) — deny because no user is available to confirm.</description></item>
698-
/// <item><description><c>"no-result"</c> (<see cref="PermissionRequestResultKind.NoResult"/>) — leave the pending request unanswered (protocol v1 only; rejected by protocol v2 servers).</description></item>
695+
/// <item><description><see cref="PermissionRequestResultKind.Approved"/> — allow this single request.</description></item>
696+
/// <item><description><see cref="PermissionRequestResultKind.Rejected"/> — deny the request.</description></item>
697+
/// <item><description><see cref="PermissionRequestResultKind.UserNotAvailable"/> — deny because no user is available to confirm.</description></item>
698+
/// <item><description><see cref="PermissionRequestResultKind.NoResult"/> — leave the pending request unanswered (protocol v1 only; rejected by protocol v2 servers).</description></item>
699699
/// </list>
700700
/// </summary>
701701
[JsonPropertyName("kind")]
@@ -706,6 +706,14 @@ public sealed class PermissionRequestResult
706706
/// </summary>
707707
[JsonPropertyName("rules")]
708708
public IList<object>? Rules { get; set; }
709+
710+
/// <summary>
711+
/// Optional human-readable feedback to forward to the LLM along with the
712+
/// decision. Mirrors the <c>feedback</c> field on the RPC-level
713+
/// <see cref="Rpc.PermissionDecision"/> type.
714+
/// </summary>
715+
[JsonPropertyName("feedback")]
716+
public string? Feedback { get; set; }
709717
}
710718

711719
/// <summary>
@@ -1167,7 +1175,8 @@ public sealed class PreToolUseHookInput
11671175
/// Unix timestamp in milliseconds when the tool use was initiated.
11681176
/// </summary>
11691177
[JsonPropertyName("timestamp")]
1170-
public long Timestamp { get; set; }
1178+
[JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))]
1179+
public DateTimeOffset Timestamp { get; set; }
11711180

11721181
/// <summary>
11731182
/// Current working directory of the session.
@@ -1249,7 +1258,8 @@ public sealed class PostToolUseHookInput
12491258
/// Unix timestamp in milliseconds when the tool execution completed.
12501259
/// </summary>
12511260
[JsonPropertyName("timestamp")]
1252-
public long Timestamp { get; set; }
1261+
[JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))]
1262+
public DateTimeOffset Timestamp { get; set; }
12531263

12541264
/// <summary>
12551265
/// Current working directory of the session.
@@ -1320,7 +1330,8 @@ public sealed class UserPromptSubmittedHookInput
13201330
/// Unix timestamp in milliseconds when the prompt was submitted.
13211331
/// </summary>
13221332
[JsonPropertyName("timestamp")]
1323-
public long Timestamp { get; set; }
1333+
[JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))]
1334+
public DateTimeOffset Timestamp { get; set; }
13241335

13251336
/// <summary>
13261337
/// Current working directory of the session.
@@ -1379,7 +1390,8 @@ public sealed class SessionStartHookInput
13791390
/// Unix timestamp in milliseconds when the session started.
13801391
/// </summary>
13811392
[JsonPropertyName("timestamp")]
1382-
public long Timestamp { get; set; }
1393+
[JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))]
1394+
public DateTimeOffset Timestamp { get; set; }
13831395

13841396
/// <summary>
13851397
/// Current working directory of the session.
@@ -1443,7 +1455,8 @@ public sealed class SessionEndHookInput
14431455
/// Unix timestamp in milliseconds when the session ended.
14441456
/// </summary>
14451457
[JsonPropertyName("timestamp")]
1446-
public long Timestamp { get; set; }
1458+
[JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))]
1459+
public DateTimeOffset Timestamp { get; set; }
14471460

14481461
/// <summary>
14491462
/// Current working directory of the session.
@@ -1521,7 +1534,8 @@ public sealed class ErrorOccurredHookInput
15211534
/// Unix timestamp in milliseconds when the error occurred.
15221535
/// </summary>
15231536
[JsonPropertyName("timestamp")]
1524-
public long Timestamp { get; set; }
1537+
[JsonConverter(typeof(UnixMillisecondsDateTimeOffsetConverter))]
1538+
public DateTimeOffset Timestamp { get; set; }
15251539

15261540
/// <summary>
15271541
/// Current working directory of the session.
@@ -2831,11 +2845,11 @@ public sealed class SessionMetadata
28312845
/// <summary>
28322846
/// Time when the session was created.
28332847
/// </summary>
2834-
public DateTime StartTime { get; set; }
2848+
public DateTimeOffset StartTime { get; set; }
28352849
/// <summary>
28362850
/// Time when the session was last modified.
28372851
/// </summary>
2838-
public DateTime ModifiedTime { get; set; }
2852+
public DateTimeOffset ModifiedTime { get; set; }
28392853
/// <summary>
28402854
/// Human-readable summary of the session.
28412855
/// </summary>
@@ -3092,16 +3106,16 @@ public sealed class GetModelsResponse
30923106
public sealed class SessionLifecycleEventMetadata
30933107
{
30943108
/// <summary>
3095-
/// ISO 8601 timestamp when the session was created.
3109+
/// Timestamp when the session was created.
30963110
/// </summary>
30973111
[JsonPropertyName("startTime")]
3098-
public string StartTime { get; set; } = string.Empty;
3112+
public DateTimeOffset StartTime { get; set; }
30993113

31003114
/// <summary>
3101-
/// ISO 8601 timestamp when the session was last modified.
3115+
/// Timestamp when the session was last modified.
31023116
/// </summary>
31033117
[JsonPropertyName("modifiedTime")]
3104-
public string ModifiedTime { get; set; } = string.Empty;
3118+
public DateTimeOffset ModifiedTime { get; set; }
31053119

31063120
/// <summary>
31073121
/// Human-readable summary of the session.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
using System.ComponentModel;
6+
using System.Text.Json;
7+
using System.Text.Json.Serialization;
8+
9+
namespace GitHub.Copilot.SDK;
10+
11+
/// <summary>Converts between JSON numeric milliseconds-since-Unix-epoch and <see cref="DateTimeOffset"/>.</summary>
12+
[EditorBrowsable(EditorBrowsableState.Never)]
13+
public sealed class UnixMillisecondsDateTimeOffsetConverter : JsonConverter<DateTimeOffset>
14+
{
15+
/// <inheritdoc />
16+
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
17+
DateTimeOffset.FromUnixTimeMilliseconds(reader.GetInt64());
18+
19+
/// <inheritdoc />
20+
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) =>
21+
writer.WriteNumberValue(value.ToUnixTimeMilliseconds());
22+
}

dotnet/test/E2E/HookLifecycleAndOutputE2ETests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public async Task Should_Invoke_OnSessionStart_Hook_On_New_Session()
4343

4444
Assert.NotEmpty(sessionStartInputs);
4545
Assert.Equal("new", sessionStartInputs[0].Source);
46-
Assert.True(sessionStartInputs[0].Timestamp > 0);
46+
Assert.True(sessionStartInputs[0].Timestamp > DateTimeOffset.UnixEpoch);
4747
Assert.False(string.IsNullOrEmpty(sessionStartInputs[0].Cwd));
4848

4949
await session.DisposeAsync();
@@ -71,7 +71,7 @@ public async Task Should_Invoke_OnUserPromptSubmitted_Hook_When_Sending_A_Messag
7171

7272
Assert.NotEmpty(userPromptInputs);
7373
Assert.Contains("Say hello", userPromptInputs[0].Prompt);
74-
Assert.True(userPromptInputs[0].Timestamp > 0);
74+
Assert.True(userPromptInputs[0].Timestamp > DateTimeOffset.UnixEpoch);
7575
Assert.False(string.IsNullOrEmpty(userPromptInputs[0].Cwd));
7676

7777
await session.DisposeAsync();
@@ -116,7 +116,7 @@ public async Task Should_Invoke_OnErrorOccurred_Hook_When_Error_Occurs()
116116
OnErrorOccurred = (input, invocation) =>
117117
{
118118
Assert.Equal(session!.SessionId, invocation.SessionId);
119-
Assert.True(input.Timestamp > 0);
119+
Assert.True(input.Timestamp > DateTimeOffset.UnixEpoch);
120120
Assert.False(string.IsNullOrEmpty(input.Cwd));
121121
Assert.False(string.IsNullOrEmpty(input.Error));
122122
Assert.Contains(input.ErrorContext, ValidErrorContexts);

0 commit comments

Comments
 (0)