Skip to content

Commit a6c64f2

Browse files
authored
fix: adding streakTracker to track streakLength across Completions and Edits (aws#2147)
1 parent 2fb896e commit a6c64f2

File tree

6 files changed

+138
-63
lines changed

6 files changed

+138
-63
lines changed

server/aws-lsp-codewhisperer/src/language-server/inline-completion/codeWhispererServer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { UserWrittenCodeTracker } from '../../shared/userWrittenCodeTracker'
4646
import { RecentEditTracker, RecentEditTrackerDefaultConfig } from './tracker/codeEditTracker'
4747
import { CursorTracker } from './tracker/cursorTracker'
4848
import { RejectedEditTracker, DEFAULT_REJECTED_EDIT_TRACKER_CONFIG } from './tracker/rejectedEditTracker'
49+
import { StreakTracker } from './tracker/streakTracker'
4950
import { getAddedAndDeletedLines, getCharacterDifferences } from './diffUtils'
5051
import {
5152
emitPerceivedLatencyTelemetry,
@@ -129,6 +130,7 @@ export const CodewhispererServerFactory =
129130
const recentEditTracker = RecentEditTracker.getInstance(logging, RecentEditTrackerDefaultConfig)
130131
const cursorTracker = CursorTracker.getInstance()
131132
const rejectedEditTracker = RejectedEditTracker.getInstance(logging, DEFAULT_REJECTED_EDIT_TRACKER_CONFIG)
133+
const streakTracker = StreakTracker.getInstance()
132134
let editsEnabled = false
133135
let isOnInlineCompletionHandlerInProgress = false
134136

@@ -317,9 +319,7 @@ export const CodewhispererServerFactory =
317319
// for the previous trigger
318320
if (ideCategory !== 'JETBRAINS') {
319321
completionSessionManager.discardSession(currentSession)
320-
const streakLength = editsEnabled
321-
? completionSessionManager.getAndUpdateStreakLength(false)
322-
: 0
322+
const streakLength = editsEnabled ? streakTracker.getAndUpdateStreakLength(false) : 0
323323
await emitUserTriggerDecisionTelemetry(
324324
telemetry,
325325
telemetryService,
@@ -405,7 +405,7 @@ export const CodewhispererServerFactory =
405405
if (session.discardInflightSessionOnNewInvocation) {
406406
session.discardInflightSessionOnNewInvocation = false
407407
completionSessionManager.discardSession(session)
408-
const streakLength = editsEnabled ? completionSessionManager.getAndUpdateStreakLength(false) : 0
408+
const streakLength = editsEnabled ? streakTracker.getAndUpdateStreakLength(false) : 0
409409
await emitUserTriggerDecisionTelemetry(
410410
telemetry,
411411
telemetryService,
@@ -664,7 +664,7 @@ export const CodewhispererServerFactory =
664664

665665
// Always emit user trigger decision at session close
666666
sessionManager.closeSession(session)
667-
const streakLength = editsEnabled ? sessionManager.getAndUpdateStreakLength(isAccepted) : 0
667+
const streakLength = editsEnabled ? streakTracker.getAndUpdateStreakLength(isAccepted) : 0
668668
await emitUserTriggerDecisionTelemetry(
669669
telemetry,
670670
telemetryService,

server/aws-lsp-codewhisperer/src/language-server/inline-completion/editCompletionHandler.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@ import { getErrorMessage, hasConnectionExpired } from '../../shared/utils'
3636
import { AmazonQError, AmazonQServiceConnectionExpiredError } from '../../shared/amazonQServiceManager/errors'
3737
import { DocumentChangedListener } from './documentChangedListener'
3838
import { EMPTY_RESULT, EDIT_DEBOUNCE_INTERVAL_MS } from './constants'
39+
import { StreakTracker } from './tracker/streakTracker'
3940

4041
export class EditCompletionHandler {
4142
private readonly editsEnabled: boolean
4243
private debounceTimeout: NodeJS.Timeout | undefined
4344
private isWaiting: boolean = false
4445
private hasDocumentChangedSinceInvocation: boolean = false
46+
private readonly streakTracker: StreakTracker
4547

4648
constructor(
4749
readonly logging: Logging,
@@ -60,6 +62,7 @@ export class EditCompletionHandler {
6062
this.editsEnabled =
6163
this.clientMetadata.initializationOptions?.aws?.awsClientCapabilities?.textDocument
6264
?.inlineCompletionWithReferences?.inlineEditSupport ?? false
65+
this.streakTracker = StreakTracker.getInstance()
6366
}
6467

6568
get codeWhispererService() {
@@ -264,7 +267,7 @@ export class EditCompletionHandler {
264267
if (currentSession && currentSession.state === 'ACTIVE') {
265268
// Emit user trigger decision at session close time for active session
266269
this.sessionManager.discardSession(currentSession)
267-
const streakLength = this.editsEnabled ? this.sessionManager.getAndUpdateStreakLength(false) : 0
270+
const streakLength = this.editsEnabled ? this.streakTracker.getAndUpdateStreakLength(false) : 0
268271
await emitUserTriggerDecisionTelemetry(
269272
this.telemetry,
270273
this.telemetryService,
@@ -335,7 +338,7 @@ export class EditCompletionHandler {
335338
if (session.discardInflightSessionOnNewInvocation) {
336339
session.discardInflightSessionOnNewInvocation = false
337340
this.sessionManager.discardSession(session)
338-
const streakLength = this.editsEnabled ? this.sessionManager.getAndUpdateStreakLength(false) : 0
341+
const streakLength = this.editsEnabled ? this.streakTracker.getAndUpdateStreakLength(false) : 0
339342
await emitUserTriggerDecisionTelemetry(
340343
this.telemetry,
341344
this.telemetryService,
@@ -359,7 +362,7 @@ export class EditCompletionHandler {
359362
this.telemetryService,
360363
session,
361364
this.documentChangedListener.timeSinceLastUserModification,
362-
this.editsEnabled ? this.sessionManager.getAndUpdateStreakLength(false) : 0
365+
this.editsEnabled ? this.streakTracker.getAndUpdateStreakLength(false) : 0
363366
)
364367
return EMPTY_RESULT
365368
}

server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.test.ts

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -673,46 +673,4 @@ describe('SessionManager', function () {
673673
assert.equal(session.getSuggestionState('id4'), 'Discard')
674674
})
675675
})
676-
677-
describe('getAndUpdateStreakLength()', function () {
678-
it('should return 0 if user rejects suggestion A', function () {
679-
const manager = SessionManager.getInstance()
680-
681-
assert.equal(manager.getAndUpdateStreakLength(false), -1)
682-
assert.equal(manager.streakLength, 0)
683-
})
684-
685-
it('should return -1 for A and 1 for B if user accepts suggestion A and rejects B', function () {
686-
const manager = SessionManager.getInstance()
687-
688-
assert.equal(manager.getAndUpdateStreakLength(true), -1)
689-
assert.equal(manager.streakLength, 1)
690-
assert.equal(manager.getAndUpdateStreakLength(false), 1)
691-
assert.equal(manager.streakLength, 0)
692-
})
693-
694-
it('should return -1 for A, -1 for B, and 2 for C if user accepts A, accepts B, and rejects C', function () {
695-
const manager = SessionManager.getInstance()
696-
697-
assert.equal(manager.getAndUpdateStreakLength(true), -1)
698-
assert.equal(manager.streakLength, 1)
699-
assert.equal(manager.getAndUpdateStreakLength(true), -1)
700-
assert.equal(manager.streakLength, 2)
701-
assert.equal(manager.getAndUpdateStreakLength(false), 2)
702-
assert.equal(manager.streakLength, 0)
703-
})
704-
705-
it('should return -1 for A, -1 for B, and 1 for C if user accepts A, make an edit, accepts B, and rejects C', function () {
706-
const manager = SessionManager.getInstance()
707-
708-
assert.equal(manager.getAndUpdateStreakLength(true), -1)
709-
assert.equal(manager.streakLength, 1)
710-
assert.equal(manager.getAndUpdateStreakLength(false), 1)
711-
assert.equal(manager.streakLength, 0)
712-
assert.equal(manager.getAndUpdateStreakLength(true), -1)
713-
assert.equal(manager.streakLength, 1)
714-
assert.equal(manager.getAndUpdateStreakLength(false), 1)
715-
assert.equal(manager.streakLength, 0)
716-
})
717-
})
718676
})

server/aws-lsp-codewhisperer/src/language-server/inline-completion/session/sessionManager.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,6 @@ export class SessionManager {
282282
private currentSession?: CodeWhispererSession
283283
private sessionsLog: CodeWhispererSession[] = []
284284
private maxHistorySize = 5
285-
streakLength: number = 0
286285
// TODO, for user decision telemetry: accepted suggestions (not necessarily the full corresponding session) should be stored for 5 minutes
287286

288287
private constructor() {}
@@ -362,16 +361,4 @@ export class SessionManager {
362361
this.currentSession.activate()
363362
}
364363
}
365-
366-
getAndUpdateStreakLength(isAccepted: boolean | undefined): number {
367-
if (!isAccepted && this.streakLength != 0) {
368-
const currentStreakLength = this.streakLength
369-
this.streakLength = 0
370-
return currentStreakLength
371-
} else if (isAccepted) {
372-
// increment streakLength everytime a suggestion is accepted.
373-
this.streakLength = this.streakLength + 1
374-
}
375-
return -1
376-
}
377364
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as assert from 'assert'
7+
import { StreakTracker } from './streakTracker'
8+
9+
describe('StreakTracker', function () {
10+
let tracker: StreakTracker
11+
12+
beforeEach(function () {
13+
StreakTracker.reset()
14+
tracker = StreakTracker.getInstance()
15+
})
16+
17+
afterEach(function () {
18+
StreakTracker.reset()
19+
})
20+
21+
describe('getInstance', function () {
22+
it('should return the same instance (singleton)', function () {
23+
const instance1 = StreakTracker.getInstance()
24+
const instance2 = StreakTracker.getInstance()
25+
assert.strictEqual(instance1, instance2)
26+
})
27+
28+
it('should create new instance after reset', function () {
29+
const instance1 = StreakTracker.getInstance()
30+
StreakTracker.reset()
31+
const instance2 = StreakTracker.getInstance()
32+
assert.notStrictEqual(instance1, instance2)
33+
})
34+
})
35+
36+
describe('getAndUpdateStreakLength', function () {
37+
it('should return -1 for undefined input', function () {
38+
const result = tracker.getAndUpdateStreakLength(undefined)
39+
assert.strictEqual(result, -1)
40+
})
41+
42+
it('should return -1 and increment streak on acceptance', function () {
43+
const result = tracker.getAndUpdateStreakLength(true)
44+
assert.strictEqual(result, -1)
45+
})
46+
47+
it('should return -1 for rejection with zero streak', function () {
48+
const result = tracker.getAndUpdateStreakLength(false)
49+
assert.strictEqual(result, -1)
50+
})
51+
52+
it('should return previous streak on rejection after acceptances', function () {
53+
tracker.getAndUpdateStreakLength(true)
54+
tracker.getAndUpdateStreakLength(true)
55+
tracker.getAndUpdateStreakLength(true)
56+
57+
const result = tracker.getAndUpdateStreakLength(false)
58+
assert.strictEqual(result, 3)
59+
})
60+
61+
it('should handle acceptance after rejection', function () {
62+
tracker.getAndUpdateStreakLength(true)
63+
tracker.getAndUpdateStreakLength(true)
64+
65+
const resetResult = tracker.getAndUpdateStreakLength(false)
66+
assert.strictEqual(resetResult, 2)
67+
68+
tracker.getAndUpdateStreakLength(true)
69+
const newResult = tracker.getAndUpdateStreakLength(true)
70+
assert.strictEqual(newResult, -1)
71+
})
72+
})
73+
74+
describe('cross-instance consistency', function () {
75+
it('should maintain state across getInstance calls', function () {
76+
const tracker1 = StreakTracker.getInstance()
77+
tracker1.getAndUpdateStreakLength(true)
78+
tracker1.getAndUpdateStreakLength(true)
79+
80+
const tracker2 = StreakTracker.getInstance()
81+
const result = tracker2.getAndUpdateStreakLength(false)
82+
assert.strictEqual(result, 2)
83+
})
84+
})
85+
})
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/**
7+
* Tracks acceptance streak across both completion and edit suggestion types.
8+
* Shared singleton to maintain consistent streak count between different code paths.
9+
*/
10+
export class StreakTracker {
11+
private static _instance?: StreakTracker
12+
private streakLength: number = 0
13+
14+
private constructor() {}
15+
16+
public static getInstance(): StreakTracker {
17+
if (!StreakTracker._instance) {
18+
StreakTracker._instance = new StreakTracker()
19+
}
20+
return StreakTracker._instance
21+
}
22+
23+
public static reset() {
24+
StreakTracker._instance = undefined
25+
}
26+
27+
/**
28+
* Updates and returns the current streak length based on acceptance status.
29+
* @param isAccepted Whether the suggestion was accepted
30+
* @returns Current streak length before update, or -1 if no change
31+
*/
32+
public getAndUpdateStreakLength(isAccepted: boolean | undefined): number {
33+
if (!isAccepted && this.streakLength !== 0) {
34+
const currentStreakLength = this.streakLength
35+
this.streakLength = 0
36+
return currentStreakLength
37+
} else if (isAccepted) {
38+
this.streakLength += 1
39+
}
40+
return -1
41+
}
42+
}

0 commit comments

Comments
 (0)