pi-provider-kimi-code 0.6.7 → 0.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -109
- package/index.ts +231 -87
- package/package.json +5 -5
- package/src/constants.ts +6 -2
- package/src/device.ts +59 -17
- package/src/models.ts +147 -62
- package/src/oauth.ts +232 -68
- package/src/payload.ts +154 -40
- package/src/stream.ts +32 -4
- package/src/usage.ts +4 -42
package/src/oauth.ts
CHANGED
|
@@ -52,43 +52,107 @@ export function readStoredOAuthCredential(providerId: string): StoredOAuthCreden
|
|
|
52
52
|
// while held, recovers stale locks from crashed processes, and retries for
|
|
53
53
|
// ~40s so a live holder running a slow network refresh inside the lock is
|
|
54
54
|
// waited out.
|
|
55
|
-
async function
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
async function acquireSharedFileLock(
|
|
56
|
+
path: string,
|
|
57
|
+
signal: AbortSignal | undefined,
|
|
58
|
+
options: { maxRetries: number; stale: number; minTimeout: number; maxTimeout: number },
|
|
59
|
+
): Promise<() => Promise<void>> {
|
|
60
|
+
for (let attempt = 0; ; attempt++) {
|
|
61
|
+
signal?.throwIfAborted();
|
|
62
|
+
try {
|
|
63
|
+
return await acquireFileLock(path, {
|
|
64
|
+
realpath: false,
|
|
65
|
+
stale: options.stale,
|
|
66
|
+
// Retry via the outer abortable sleep below. proper-lockfile's own
|
|
67
|
+
// retry loop is not signal-aware and would otherwise ignore an abort
|
|
68
|
+
// for the full configured retry window.
|
|
69
|
+
retries: 0,
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const code =
|
|
73
|
+
typeof error === "object" && error !== null && "code" in error
|
|
74
|
+
? String((error as { code?: unknown }).code)
|
|
75
|
+
: undefined;
|
|
76
|
+
if (code !== "ELOCKED" || attempt >= options.maxRetries) throw error;
|
|
77
|
+
const baseDelay = Math.min(options.minTimeout * 2 ** attempt, options.maxTimeout);
|
|
78
|
+
await sleep(baseDelay * (1 + Math.random()), signal);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function acquireAuthFileLock(
|
|
84
|
+
authPath: string,
|
|
85
|
+
signal?: AbortSignal,
|
|
86
|
+
): Promise<() => Promise<void>> {
|
|
87
|
+
return acquireSharedFileLock(authPath, signal, {
|
|
88
|
+
maxRetries: 10,
|
|
58
89
|
stale: 30_000,
|
|
59
|
-
|
|
90
|
+
minTimeout: 100,
|
|
91
|
+
maxTimeout: 10_000,
|
|
60
92
|
});
|
|
61
93
|
}
|
|
62
94
|
|
|
63
|
-
async function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
): Promise<void> {
|
|
67
|
-
const AuthStorage = piAgentRuntime.AuthStorage;
|
|
68
|
-
if (AuthStorage) {
|
|
69
|
-
AuthStorage.create().set(providerId, credential);
|
|
70
|
-
return;
|
|
95
|
+
async function acquireKimiCodeRefreshLock(signal?: AbortSignal): Promise<() => Promise<void>> {
|
|
96
|
+
if (process.platform === "win32" || process.env.KIMI_DISABLE_OAUTH_LOCK === "1") {
|
|
97
|
+
return async () => {};
|
|
71
98
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
const target = join(
|
|
100
|
+
process.env.KIMI_CODE_HOME || join(os.homedir(), ".kimi-code"),
|
|
101
|
+
"oauth",
|
|
102
|
+
"kimi-code",
|
|
103
|
+
);
|
|
104
|
+
mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
|
|
105
|
+
writeFileSync(target, "", { encoding: "utf-8", flag: "a" });
|
|
106
|
+
return acquireSharedFileLock(target, signal, {
|
|
107
|
+
maxRetries: 120,
|
|
108
|
+
stale: 5_000,
|
|
109
|
+
minTimeout: 500,
|
|
110
|
+
maxTimeout: 1_000,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface LockedOAuthCredential {
|
|
115
|
+
credential: StoredOAuthCredential | null;
|
|
116
|
+
store(credential: StoredOAuthCredential): void;
|
|
117
|
+
release(): Promise<void>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function lockStoredOAuthCredential(
|
|
121
|
+
providerId: string,
|
|
122
|
+
signal?: AbortSignal,
|
|
123
|
+
): Promise<LockedOAuthCredential> {
|
|
124
|
+
// Hold the same lock Pi uses across the complete read-refresh-write
|
|
125
|
+
// transaction. Kimi rotates refresh tokens, so locking only the write lets
|
|
126
|
+
// concurrent Pi processes exchange the same token twice.
|
|
77
127
|
const authPath = join(piAgentRuntime.getAgentDir(), "auth.json");
|
|
78
|
-
|
|
128
|
+
mkdirSync(dirname(authPath), { recursive: true, mode: 0o700 });
|
|
129
|
+
if (!existsSync(authPath)) {
|
|
130
|
+
writeFileSync(authPath, "{}", { encoding: "utf-8", mode: 0o600 });
|
|
131
|
+
chmodSync(authPath, 0o600);
|
|
132
|
+
}
|
|
133
|
+
const release = await acquireAuthFileLock(authPath, signal);
|
|
134
|
+
let data: Record<string, unknown> = {};
|
|
79
135
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
data =
|
|
83
|
-
} catch {
|
|
84
|
-
// Missing or unreadable auth.json — start from an empty store.
|
|
136
|
+
const parsed = JSON.parse(readFileSync(authPath, "utf-8")) as unknown;
|
|
137
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
138
|
+
data = parsed as Record<string, unknown>;
|
|
85
139
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
chmodSync(authPath, 0o600);
|
|
89
|
-
} finally {
|
|
90
|
-
await release();
|
|
140
|
+
} catch {
|
|
141
|
+
// Missing or unreadable auth.json — treat it as an empty store.
|
|
91
142
|
}
|
|
143
|
+
const stored = data[providerId] as StoredCredential | undefined;
|
|
144
|
+
return {
|
|
145
|
+
credential: stored?.type === "oauth" ? (stored as unknown as StoredOAuthCredential) : null,
|
|
146
|
+
store(credential) {
|
|
147
|
+
data[providerId] = credential;
|
|
148
|
+
writeFileSync(authPath, JSON.stringify(data, null, 2), {
|
|
149
|
+
encoding: "utf-8",
|
|
150
|
+
mode: 0o600,
|
|
151
|
+
});
|
|
152
|
+
chmodSync(authPath, 0o600);
|
|
153
|
+
},
|
|
154
|
+
release,
|
|
155
|
+
};
|
|
92
156
|
}
|
|
93
157
|
|
|
94
158
|
import { CLIENT_ID, PROVIDER_ID, RETRYABLE_REFRESH_STATUSES, getOAuthHost } from "./constants.ts";
|
|
@@ -339,14 +403,10 @@ function readKimiCliCredentials(): KimiCliCredentialsFile | null {
|
|
|
339
403
|
return null;
|
|
340
404
|
}
|
|
341
405
|
|
|
342
|
-
function kimiCodeCredentialExists(): boolean {
|
|
343
|
-
return existsSync(getKimiCodeCredentialPath());
|
|
344
|
-
}
|
|
345
|
-
|
|
346
406
|
function writeKimiCodeCredentials(access: string, refresh: string, expiresMs: number): void {
|
|
347
407
|
const path = getKimiCodeCredentialPath();
|
|
348
408
|
const dir = dirname(path);
|
|
349
|
-
mkdirSync(dir, { recursive: true });
|
|
409
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
350
410
|
const data: KimiCliCredentialsFile = {
|
|
351
411
|
access_token: access,
|
|
352
412
|
refresh_token: refresh,
|
|
@@ -355,7 +415,24 @@ function writeKimiCodeCredentials(access: string, refresh: string, expiresMs: nu
|
|
|
355
415
|
token_type: "Bearer",
|
|
356
416
|
expires_in: Math.max(0, Math.floor((expiresMs - Date.now()) / 1000)),
|
|
357
417
|
};
|
|
358
|
-
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
418
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
419
|
+
chmodSync(path, 0o600);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
interface LockedKimiCodeCredentials {
|
|
423
|
+
credential: KimiCliCredentialsFile | null;
|
|
424
|
+
store(access: string, refresh: string, expiresMs: number): void;
|
|
425
|
+
release(): Promise<void>;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function lockKimiCodeCredentials(signal?: AbortSignal): Promise<LockedKimiCodeCredentials> {
|
|
429
|
+
const release = await acquireKimiCodeRefreshLock(signal);
|
|
430
|
+
const credential = readKimiCliCredentials();
|
|
431
|
+
return {
|
|
432
|
+
credential,
|
|
433
|
+
store: writeKimiCodeCredentials,
|
|
434
|
+
release,
|
|
435
|
+
};
|
|
359
436
|
}
|
|
360
437
|
|
|
361
438
|
async function tryReuseKimiCliCredentials(
|
|
@@ -476,26 +553,83 @@ export async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<Kim
|
|
|
476
553
|
export async function refreshKimiCodeToken(
|
|
477
554
|
credentials: OAuthCredentials,
|
|
478
555
|
): Promise<KimiOAuthCredentials> {
|
|
479
|
-
const
|
|
480
|
-
let token: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
556
|
+
const kimiLocked = await lockKimiCodeCredentials();
|
|
481
557
|
try {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
558
|
+
const initialKimiCred = kimiLocked.credential;
|
|
559
|
+
const initialKimiExpiresMs = (initialKimiCred?.expires_at ?? 0) * 1000;
|
|
560
|
+
const peerHasNewerValidAccess =
|
|
561
|
+
initialKimiCred?.access_token !== undefined &&
|
|
562
|
+
initialKimiCred.access_token !== credentials.access &&
|
|
563
|
+
Date.now() < initialKimiExpiresMs;
|
|
564
|
+
const activeRefreshToken = peerHasNewerValidAccess
|
|
565
|
+
? initialKimiCred.refresh_token!
|
|
566
|
+
: credentials.refresh;
|
|
567
|
+
const activeAccessToken = peerHasNewerValidAccess
|
|
568
|
+
? initialKimiCred.access_token!
|
|
569
|
+
: credentials.access;
|
|
570
|
+
if (peerHasNewerValidAccess && initialKimiCred.refresh_token !== credentials.refresh) {
|
|
571
|
+
const extras = await discoverKimiModelMetadata(activeAccessToken);
|
|
572
|
+
return {
|
|
573
|
+
access: activeAccessToken,
|
|
574
|
+
refresh: activeRefreshToken,
|
|
575
|
+
expires: initialKimiExpiresMs,
|
|
576
|
+
...extras,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
let token: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
580
|
+
try {
|
|
581
|
+
token = await refreshAccessToken(activeRefreshToken);
|
|
582
|
+
} catch (error) {
|
|
583
|
+
// Lock-disabled platforms (Windows / KIMI_DISABLE_OAUTH_LOCK=1) use
|
|
584
|
+
// storage re-read as the only coordinator. A peer may have rotated the
|
|
585
|
+
// shared sidecar while this process was exchanging the old refresh
|
|
586
|
+
// token; recover from that newer valid credential instead of surfacing
|
|
587
|
+
// the stale-token invalid_grant as a login failure.
|
|
588
|
+
await sleep(100);
|
|
589
|
+
const peerCredential = readKimiCliCredentials();
|
|
590
|
+
const peerAccess = peerCredential?.access_token;
|
|
591
|
+
const peerRefresh = peerCredential?.refresh_token;
|
|
592
|
+
const peerExpiresMs = (peerCredential?.expires_at ?? 0) * 1000;
|
|
593
|
+
if (
|
|
594
|
+
peerAccess !== undefined &&
|
|
595
|
+
peerRefresh !== undefined &&
|
|
596
|
+
peerAccess !== credentials.access &&
|
|
597
|
+
Date.now() < peerExpiresMs
|
|
598
|
+
) {
|
|
599
|
+
const extras = await discoverKimiModelMetadata(peerAccess);
|
|
600
|
+
return {
|
|
601
|
+
access: peerAccess,
|
|
602
|
+
refresh: peerRefresh,
|
|
603
|
+
expires: peerExpiresMs,
|
|
604
|
+
...extras,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
487
607
|
throw error;
|
|
488
608
|
}
|
|
609
|
+
const expiresMs = Date.now() + token.expires_in * 1000;
|
|
610
|
+
const kimiCredentialAfterRefresh = readKimiCliCredentials();
|
|
611
|
+
const peerAccess = kimiCredentialAfterRefresh?.access_token;
|
|
612
|
+
const peerRefresh = kimiCredentialAfterRefresh?.refresh_token;
|
|
613
|
+
const peerExpiresMs = (kimiCredentialAfterRefresh?.expires_at ?? 0) * 1000;
|
|
614
|
+
const accessToken =
|
|
615
|
+
peerAccess !== undefined && peerAccess !== activeAccessToken && Date.now() < peerExpiresMs
|
|
616
|
+
? peerAccess
|
|
617
|
+
: token.access_token;
|
|
618
|
+
const refreshToken = accessToken === peerAccess ? peerRefresh! : token.refresh_token;
|
|
619
|
+
const effectiveExpiresMs = accessToken === peerAccess ? peerExpiresMs : expiresMs;
|
|
620
|
+
if (accessToken !== peerAccess) {
|
|
621
|
+
kimiLocked.store(accessToken, refreshToken, effectiveExpiresMs);
|
|
622
|
+
}
|
|
623
|
+
const extras = await discoverKimiModelMetadata(accessToken);
|
|
624
|
+
return {
|
|
625
|
+
access: accessToken,
|
|
626
|
+
refresh: refreshToken,
|
|
627
|
+
expires: effectiveExpiresMs,
|
|
628
|
+
...extras,
|
|
629
|
+
};
|
|
630
|
+
} finally {
|
|
631
|
+
await kimiLocked.release().catch(() => {});
|
|
489
632
|
}
|
|
490
|
-
const expiresMs = Date.now() + token.expires_in * 1000;
|
|
491
|
-
writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
|
|
492
|
-
const extras = await discoverKimiModelMetadata(token.access_token);
|
|
493
|
-
return {
|
|
494
|
-
access: token.access_token,
|
|
495
|
-
refresh: token.refresh_token,
|
|
496
|
-
expires: expiresMs,
|
|
497
|
-
...extras,
|
|
498
|
-
};
|
|
499
633
|
}
|
|
500
634
|
|
|
501
635
|
// =============================================================================
|
|
@@ -524,7 +658,13 @@ export function isKimiAuthErrorMessage(message: unknown): boolean {
|
|
|
524
658
|
/\b401\b/.test(text) ||
|
|
525
659
|
text.includes("unauthorized") ||
|
|
526
660
|
text.includes("incorrect api key") ||
|
|
527
|
-
text.includes("invalid api key")
|
|
661
|
+
text.includes("invalid api key") ||
|
|
662
|
+
// Kimi's production 401 body: "The API Key appears to be invalid or may
|
|
663
|
+
// have expired..." with type "invalid_authentication_error". Neither
|
|
664
|
+
// contains the substrings above, so without these the digit fallback is
|
|
665
|
+
// the only match and vanishes when a transport strips the status prefix.
|
|
666
|
+
text.includes("api key appears to be invalid") ||
|
|
667
|
+
text.includes("invalid_authentication_error")
|
|
528
668
|
);
|
|
529
669
|
}
|
|
530
670
|
|
|
@@ -532,21 +672,23 @@ export async function refreshKimiAuthToken(
|
|
|
532
672
|
currentKey: string,
|
|
533
673
|
options: { signal?: AbortSignal } = {},
|
|
534
674
|
): Promise<string | null> {
|
|
675
|
+
let locked: LockedOAuthCredential | undefined;
|
|
676
|
+
let kimiLocked: LockedKimiCodeCredentials | undefined;
|
|
535
677
|
try {
|
|
536
|
-
|
|
537
|
-
const piOAuth =
|
|
678
|
+
locked = await lockStoredOAuthCredential(PROVIDER_ID, options.signal);
|
|
679
|
+
const piOAuth = locked.credential;
|
|
538
680
|
|
|
539
681
|
if (piOAuth) {
|
|
540
682
|
if (piOAuth.access !== currentKey && Date.now() < piOAuth.expires) {
|
|
541
|
-
console.error("[kimi-coding] auth refresh: reusing newer on-disk token");
|
|
542
683
|
return piOAuth.access;
|
|
543
684
|
}
|
|
544
685
|
|
|
545
|
-
console.error("[kimi-coding] auth refresh: requesting new access token");
|
|
546
686
|
let refreshed: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
687
|
+
kimiLocked = await lockKimiCodeCredentials(options.signal);
|
|
547
688
|
try {
|
|
548
689
|
refreshed = await refreshAccessToken(piOAuth.refresh, { signal: options.signal });
|
|
549
690
|
} catch (error) {
|
|
691
|
+
const kimiCred = readKimiCliCredentials();
|
|
550
692
|
if (!kimiCred?.refresh_token || kimiCred.refresh_token === piOAuth.refresh) throw error;
|
|
551
693
|
const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
|
|
552
694
|
if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
|
|
@@ -557,15 +699,30 @@ export async function refreshKimiAuthToken(
|
|
|
557
699
|
refresh: kimiCred.refresh_token,
|
|
558
700
|
expires: kimiExpiresMs,
|
|
559
701
|
};
|
|
560
|
-
|
|
561
|
-
console.error("[kimi-coding] auth refresh: recovered newer kimi-code token");
|
|
702
|
+
locked.store(recovered);
|
|
562
703
|
return recovered.access;
|
|
563
704
|
}
|
|
564
|
-
console.error("[kimi-coding] auth refresh: pi token rejected, trying kimi-code token");
|
|
565
705
|
refreshed = await refreshAccessToken(kimiCred.refresh_token, { signal: options.signal });
|
|
566
706
|
}
|
|
567
707
|
|
|
568
708
|
const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
|
|
709
|
+
const kimiCredentialAfterRefresh = readKimiCliCredentials();
|
|
710
|
+
const peerAccess = kimiCredentialAfterRefresh?.access_token;
|
|
711
|
+
const peerExpiresMs = (kimiCredentialAfterRefresh?.expires_at ?? 0) * 1000;
|
|
712
|
+
const peerIsNewerValid =
|
|
713
|
+
peerAccess !== undefined && peerAccess !== currentKey && Date.now() < peerExpiresMs;
|
|
714
|
+
if (peerIsNewerValid) {
|
|
715
|
+
const peerCred: StoredOAuthCredential = {
|
|
716
|
+
...piOAuth,
|
|
717
|
+
type: "oauth",
|
|
718
|
+
access: peerAccess,
|
|
719
|
+
refresh: kimiCredentialAfterRefresh!.refresh_token!,
|
|
720
|
+
expires: peerExpiresMs,
|
|
721
|
+
};
|
|
722
|
+
locked.store(peerCred);
|
|
723
|
+
return peerCred.access;
|
|
724
|
+
}
|
|
725
|
+
kimiLocked.store(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
|
|
569
726
|
const newCred: StoredOAuthCredential = {
|
|
570
727
|
...piOAuth,
|
|
571
728
|
type: "oauth",
|
|
@@ -573,32 +730,32 @@ export async function refreshKimiAuthToken(
|
|
|
573
730
|
refresh: refreshed.refresh_token,
|
|
574
731
|
expires: newExpiresMs,
|
|
575
732
|
};
|
|
576
|
-
|
|
577
|
-
writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
|
|
578
|
-
console.error("[kimi-coding] auth refresh: new token persisted");
|
|
733
|
+
locked.store(newCred);
|
|
579
734
|
return newCred.access;
|
|
580
735
|
}
|
|
581
736
|
|
|
737
|
+
kimiLocked = await lockKimiCodeCredentials(options.signal);
|
|
738
|
+
const kimiCred = kimiLocked.credential;
|
|
582
739
|
if (!kimiCred) {
|
|
583
|
-
console.error(
|
|
584
|
-
`[kimi-coding] auth refresh skipped: no OAuth credentials for ${PROVIDER_ID} on disk`,
|
|
585
|
-
);
|
|
586
740
|
return null;
|
|
587
741
|
}
|
|
588
742
|
|
|
589
743
|
const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
|
|
590
744
|
if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
|
|
591
|
-
console.error("[kimi-coding] auth refresh: reusing newer kimi-code token");
|
|
592
745
|
return kimiCred.access_token!;
|
|
593
746
|
}
|
|
594
747
|
|
|
595
|
-
console.error("[kimi-coding] auth refresh: requesting new access token via kimi-code");
|
|
596
748
|
const refreshed = await refreshAccessToken(kimiCred.refresh_token!, {
|
|
597
749
|
signal: options.signal,
|
|
598
750
|
});
|
|
599
751
|
const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
|
|
600
|
-
|
|
601
|
-
|
|
752
|
+
const kimiCredentialAfterRefresh = readKimiCliCredentials();
|
|
753
|
+
const peerAccess = kimiCredentialAfterRefresh?.access_token;
|
|
754
|
+
const peerExpiresMs = (kimiCredentialAfterRefresh?.expires_at ?? 0) * 1000;
|
|
755
|
+
if (peerAccess !== undefined && peerAccess !== currentKey && Date.now() < peerExpiresMs) {
|
|
756
|
+
return peerAccess;
|
|
757
|
+
}
|
|
758
|
+
kimiLocked.store(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
|
|
602
759
|
return refreshed.access_token;
|
|
603
760
|
} catch (err) {
|
|
604
761
|
if (options.signal?.aborted) return null;
|
|
@@ -608,5 +765,12 @@ export async function refreshKimiAuthToken(
|
|
|
608
765
|
console.error("[kimi-coding] auth refresh failed:", err);
|
|
609
766
|
}
|
|
610
767
|
return null;
|
|
768
|
+
} finally {
|
|
769
|
+
if (kimiLocked) {
|
|
770
|
+
await kimiLocked.release().catch(() => {});
|
|
771
|
+
}
|
|
772
|
+
if (locked) {
|
|
773
|
+
await locked.release().catch(() => {});
|
|
774
|
+
}
|
|
611
775
|
}
|
|
612
776
|
}
|