@sunerpy/opencode-kiro-auth 0.14.2 → 0.15.1

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.
@@ -1,6 +1,7 @@
1
1
  import { RegionSchema } from '../../plugin/config/schema.js';
2
2
  import { isRefreshTokenDead } from '../../plugin/health.js';
3
3
  import * as logger from '../../plugin/logger.js';
4
+ import { PLUGIN_VERSION } from '../../version.js';
4
5
  import { IdcAuthMethod } from './idc-auth-method.js';
5
6
  import { isInteractiveTty, ttyConfirm, ttySelect } from './tty-menu.js';
6
7
  export class AuthHandler {
@@ -91,7 +92,7 @@ export class AuthHandler {
91
92
  }
92
93
  const currentAccounts = this.accountManager.getAccounts?.() ?? [];
93
94
  const usageSummary = this.buildUsageSummary(currentAccounts);
94
- const firstLabelBase = 'Add account · AWS Builder ID / IAM Identity Center';
95
+ const firstLabelBase = `Add account · AWS Builder ID / IAM Identity Center · plugin v${PLUGIN_VERSION}`;
95
96
  const firstLabel = usageSummary ? `${firstLabelBase} ${usageSummary}` : firstLabelBase;
96
97
  const idcMethod = new IdcAuthMethod(this.config, this.repository, this.accountManager);
97
98
  const configStartUrl = this.config.idc_start_url;
@@ -14,6 +14,7 @@ import { RetryStrategy } from './retry-strategy.js';
14
14
  import { SdkEventStreamIterationError, UpstreamUnexpectedError } from './stream-error.js';
15
15
  const KIRO_API_PATTERN = /^(https?:\/\/)?q\.[a-z0-9-]+\.amazonaws\.com/;
16
16
  const REAUTH_FAILURE_COOLDOWN_MS = 60000;
17
+ const MAX_STREAM_ATTEMPTS = 3;
17
18
  function describeError(error, depth = 0) {
18
19
  if (!(error instanceof Error))
19
20
  return String(error);
@@ -182,6 +183,18 @@ export class RequestHandler {
182
183
  continue;
183
184
  }
184
185
  const sdkPrep = this.prepareSdkRequest(init?.body, model, auth, think, budget, showToast);
186
+ const streamAttempt = streamFailureCount + 1;
187
+ const streamLogDetails = (details = {}) => ({
188
+ conversationId: sdkPrep.conversationId,
189
+ model,
190
+ effectiveModel: sdkPrep.effectiveModel,
191
+ region: sdkPrep.region,
192
+ account: acc.email,
193
+ accountId: acc.id,
194
+ streamAttempt,
195
+ maxStreamAttempts: MAX_STREAM_ATTEMPTS,
196
+ ...details
197
+ });
185
198
  if (this.config.enable_log_effort_debug) {
186
199
  try {
187
200
  logger.log('[effort-debug] request effort resolution', {
@@ -231,6 +244,7 @@ export class RequestHandler {
231
244
  if (this.config.sdk_response_timeout_enabled) {
232
245
  const messageContext = sdkPrep.conversationState.currentMessage?.userInputMessage?.userInputMessageContext;
233
246
  beginUpstreamWait('SDK response', this.config.sdk_response_timeout_ms, {
247
+ conversationId: sdkPrep.conversationId,
234
248
  model,
235
249
  effectiveModel: sdkPrep.effectiveModel,
236
250
  effort: sdkPrep.effort,
@@ -261,6 +275,7 @@ export class RequestHandler {
261
275
  if (!this.config.stream_event_timeout_enabled)
262
276
  return;
263
277
  beginUpstreamWait('stream event', this.config.request_timeout_ms, {
278
+ conversationId: sdkPrep.conversationId,
264
279
  model,
265
280
  effectiveModel: sdkPrep.effectiveModel,
266
281
  region: sdkPrep.region,
@@ -268,20 +283,32 @@ export class RequestHandler {
268
283
  });
269
284
  },
270
285
  onUpstreamWaitEnd: endUpstreamWait,
271
- onIterationError: (error, afterCompletionMetadata) => logger.warn('Kiro SDK event stream iteration failed', {
272
- model,
273
- effectiveModel: sdkPrep.effectiveModel,
274
- region: sdkPrep.region,
275
- platform: process.platform,
276
- afterCompletionMetadata,
277
- recovered: afterCompletionMetadata,
278
- error: describeError(error)
279
- }),
286
+ onIterationError: (error, afterCompletionMetadata) => {
287
+ if (!afterCompletionMetadata)
288
+ return;
289
+ logger.log('Kiro SDK event stream closed after completion metadata', streamLogDetails({
290
+ outcome: 'ignored_after_completion_metadata',
291
+ platform: process.platform,
292
+ afterCompletionMetadata,
293
+ error: describeError(error)
294
+ }));
295
+ },
280
296
  onComplete: completeRequest,
281
297
  onTerminal: cleanupRequest,
282
298
  onCancel: (reason) => requestController.abort(reason),
283
- mapError: (error) => new UpstreamUnexpectedError(error, true)
299
+ mapError: (error) => {
300
+ logger.error('Kiro SDK event stream iteration failed', streamLogDetails({
301
+ outcome: 'terminated_after_output',
302
+ platform: process.platform,
303
+ emittedOutput: true,
304
+ error: describeError(error)
305
+ }));
306
+ return new UpstreamUnexpectedError(error, true);
307
+ }
284
308
  });
309
+ if (streamFailureCount > 0) {
310
+ logger.log('Kiro SDK event stream retry recovered', streamLogDetails({ outcome: 'recovered', attempts: streamAttempt }));
311
+ }
285
312
  if (sdkPrep.streaming) {
286
313
  responseOwnsLifecycle = true;
287
314
  }
@@ -296,9 +323,17 @@ export class RequestHandler {
296
323
  if (e instanceof SdkEventStreamIterationError) {
297
324
  streamFailureCount++;
298
325
  const streamError = new UpstreamUnexpectedError(e, false);
299
- if (streamFailureCount >= 3)
326
+ if (streamFailureCount >= MAX_STREAM_ATTEMPTS) {
327
+ logger.error('Kiro SDK event stream iteration failed', streamLogDetails({
328
+ outcome: 'exhausted',
329
+ platform: process.platform,
330
+ attempts: streamFailureCount,
331
+ error: describeError(e)
332
+ }));
300
333
  return streamError.toResponse();
301
- await this.sleep(this.getStreamRetryDelay(streamFailureCount), signal);
334
+ }
335
+ const delayMs = this.getStreamRetryDelay(streamFailureCount);
336
+ await this.sleep(delayMs, signal);
302
337
  if (streamFailureCount === 1) {
303
338
  forcedStreamAccount = acc;
304
339
  }
@@ -306,6 +341,14 @@ export class RequestHandler {
306
341
  forcedStreamAccount =
307
342
  (await this.accountSelector.selectAlternativeAccount(new Set([acc.id]))) ?? acc;
308
343
  }
344
+ logger.warn('Kiro SDK event stream iteration failed', streamLogDetails({
345
+ outcome: 'retrying',
346
+ platform: process.platform,
347
+ nextAttempt: streamFailureCount + 1,
348
+ delayMs,
349
+ nextAccount: forcedStreamAccount.email,
350
+ error: describeError(e)
351
+ }));
309
352
  continue;
310
353
  }
311
354
  if (sendResolved)
@@ -15,14 +15,12 @@ const DATABASE_LOCK_MIN_BACKOFF_MS = 25;
15
15
  const DATABASE_LOCK_MAX_BACKOFF_MS = 250;
16
16
  const REFRESH_LOCK_OPTIONS = {
17
17
  stale: 15000,
18
- retries: {
19
- retries: 10,
20
- minTimeout: 100,
21
- maxTimeout: 1000,
22
- factor: 2
23
- },
18
+ retries: 0,
24
19
  realpath: false
25
20
  };
21
+ const REFRESH_LOCK_DEADLINE_MS = 15000;
22
+ const REFRESH_LOCK_MIN_BACKOFF_MS = 25;
23
+ const REFRESH_LOCK_MAX_BACKOFF_MS = 250;
26
24
  const KEEP_ALIVE_LOCK_OPTIONS = {
27
25
  stale: 120000,
28
26
  retries: 0,
@@ -36,8 +34,8 @@ function blockingBackoff(ms) {
36
34
  function isLockContention(e) {
37
35
  return typeof e === 'object' && e !== null && 'code' in e && e.code === 'ELOCKED';
38
36
  }
39
- function asyncBackoff(attempt, remainingMs) {
40
- const ceiling = Math.min(DATABASE_LOCK_MIN_BACKOFF_MS * 2 ** Math.min(attempt, 4), DATABASE_LOCK_MAX_BACKOFF_MS, remainingMs);
37
+ function asyncBackoff(attempt, remainingMs, minBackoffMs, maxBackoffMs) {
38
+ const ceiling = Math.min(minBackoffMs * 2 ** Math.min(attempt, 4), maxBackoffMs, remainingMs);
41
39
  const floor = Math.max(1, Math.floor(ceiling / 2));
42
40
  const delay = floor + Math.floor(Math.random() * (ceiling - floor + 1));
43
41
  return new Promise((resolve) => setTimeout(resolve, delay));
@@ -55,7 +53,24 @@ async function acquireDatabaseLock(dbPath) {
55
53
  const remainingMs = deadline - Date.now();
56
54
  if (!isLockContention(e) || remainingMs <= 0)
57
55
  throw e;
58
- await asyncBackoff(attempt++, remainingMs);
56
+ await asyncBackoff(attempt++, remainingMs, DATABASE_LOCK_MIN_BACKOFF_MS, DATABASE_LOCK_MAX_BACKOFF_MS);
57
+ }
58
+ }
59
+ }
60
+ async function acquireRefreshLock(lockPath) {
61
+ // A deadline avoids fixed retry-count starvation; jitter keeps contenders
62
+ // from repeatedly attempting the atomic mkdir in lockstep.
63
+ const deadline = Date.now() + REFRESH_LOCK_DEADLINE_MS;
64
+ let attempt = 0;
65
+ for (;;) {
66
+ try {
67
+ return await lockfile.lock(lockPath, REFRESH_LOCK_OPTIONS);
68
+ }
69
+ catch (e) {
70
+ const remainingMs = deadline - Date.now();
71
+ if (!isLockContention(e) || remainingMs <= 0)
72
+ throw e;
73
+ await asyncBackoff(attempt++, remainingMs, REFRESH_LOCK_MIN_BACKOFF_MS, REFRESH_LOCK_MAX_BACKOFF_MS);
59
74
  }
60
75
  }
61
76
  }
@@ -121,7 +136,7 @@ export async function withRefreshLock(accountId, fn) {
121
136
  }
122
137
  let release = null;
123
138
  try {
124
- release = await lockfile.lock(lockPath, REFRESH_LOCK_OPTIONS);
139
+ release = await acquireRefreshLock(lockPath);
125
140
  return await fn();
126
141
  }
127
142
  finally {
@@ -0,0 +1 @@
1
+ export declare const PLUGIN_VERSION: string;
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from 'node:fs';
2
+ function readPluginVersion() {
3
+ try {
4
+ const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
5
+ if (typeof packageJson.version === 'string' && packageJson.version.trim()) {
6
+ return packageJson.version;
7
+ }
8
+ }
9
+ catch {
10
+ // Keep authentication available if package metadata cannot be read.
11
+ }
12
+ return 'unknown';
13
+ }
14
+ export const PLUGIN_VERSION = readPluginVersion();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunerpy/opencode-kiro-auth",
3
- "version": "0.14.2",
3
+ "version": "0.15.1",
4
4
  "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",