@sunerpy/opencode-kiro-auth 0.15.2 → 0.15.3

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 CHANGED
@@ -109,6 +109,14 @@ multi-account or long-idle setups, enable
109
109
  (`token_keepalive_enabled: true`) to keep idle accounts' tokens fresh while
110
110
  OpenCode is running.
111
111
 
112
+ For long-running agent tasks that are frequently interrupted by upstream
113
+ `ECONNRESET` event-stream failures, enable
114
+ `"stream_buffer_until_complete": true`. The plugin then withholds a failed
115
+ attempt from OpenCode and safely retries it instead of exposing a partial
116
+ assistant response or partial tool call. See
117
+ [stream recovery configuration](docs/CONFIGURATION.md#options) for the latency
118
+ and quota tradeoffs.
119
+
112
120
  Paid-overage protection is on by default; see
113
121
  [Overage protection](docs/CONFIGURATION.md#overage-protection) before disabling
114
122
  `stop_on_overage`.
@@ -231,6 +239,12 @@ accounts", `/connect` vs `opencode auth login`, and Kiro CLI OAuth users whose
231
239
  sync doesn't start — are covered in
232
240
  [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md).
233
241
 
242
+ `Kiro upstream event stream failed unexpectedly` means the upstream HTTP 200
243
+ event stream ended before completion metadata. Live-stream mode cannot safely
244
+ replay after output because that could duplicate text or execute a tool twice.
245
+ Enable `stream_buffer_until_complete` when task continuity is more important
246
+ than seeing tokens arrive live.
247
+
234
248
  ## Migration
235
249
 
236
250
  If you're upgrading from a version that used the provider id `kiro` instead of
@@ -14,7 +14,6 @@ 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;
18
17
  function describeError(error, depth = 0) {
19
18
  if (!(error instanceof Error))
20
19
  return String(error);
@@ -192,7 +191,8 @@ export class RequestHandler {
192
191
  account: acc.email,
193
192
  accountId: acc.id,
194
193
  streamAttempt,
195
- maxStreamAttempts: MAX_STREAM_ATTEMPTS,
194
+ maxStreamAttempts: this.config.stream_max_attempts,
195
+ streamDeliveryMode: this.config.stream_buffer_until_complete ? 'buffered' : 'live',
196
196
  ...details
197
197
  });
198
198
  if (this.config.enable_log_effort_debug) {
@@ -296,6 +296,7 @@ export class RequestHandler {
296
296
  onComplete: completeRequest,
297
297
  onTerminal: cleanupRequest,
298
298
  onCancel: (reason) => requestController.abort(reason),
299
+ bufferUntilComplete: this.config.stream_buffer_until_complete,
299
300
  mapError: (error) => {
300
301
  logger.error('Kiro SDK event stream iteration failed', streamLogDetails({
301
302
  outcome: 'terminated_after_output',
@@ -323,7 +324,7 @@ export class RequestHandler {
323
324
  if (e instanceof SdkEventStreamIterationError) {
324
325
  streamFailureCount++;
325
326
  const streamError = new UpstreamUnexpectedError(e, false);
326
- if (streamFailureCount >= MAX_STREAM_ATTEMPTS) {
327
+ if (streamFailureCount >= this.config.stream_max_attempts) {
327
328
  logger.error('Kiro SDK event stream iteration failed', streamLogDetails({
328
329
  outcome: 'exhausted',
329
330
  platform: process.platform,
@@ -10,6 +10,7 @@ export interface SdkResponseLifecycle {
10
10
  onTerminal?: () => void;
11
11
  onCancel?: (reason: unknown) => void;
12
12
  mapError?: (error: SdkEventStreamIterationError, emittedOutput: true) => unknown;
13
+ bufferUntilComplete?: boolean;
13
14
  }
14
15
  export declare class ResponseHandler {
15
16
  handleSuccess(response: Response, model: string, conversationId: string, streaming: boolean): Promise<Response>;
@@ -115,6 +115,18 @@ function isSemanticChunk(chunk) {
115
115
  function encodeSseChunk(chunk) {
116
116
  return new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`);
117
117
  }
118
+ function bufferedSseResponse(chunks) {
119
+ let index = 0;
120
+ return new Response(new ReadableStream({
121
+ pull(controller) {
122
+ const chunk = chunks[index++];
123
+ if (chunk)
124
+ controller.enqueue(chunk);
125
+ if (index >= chunks.length)
126
+ controller.close();
127
+ }
128
+ }, { highWaterMark: 0 }), { headers: { 'Content-Type': 'text/event-stream' } });
129
+ }
118
130
  export class ResponseHandler {
119
131
  async handleSuccess(response, model, conversationId, streaming) {
120
132
  if (streaming) {
@@ -148,22 +160,34 @@ export class ResponseHandler {
148
160
  const wrapped = wrapSdkEventStream(sdkResponse, lifecycle.signal, lifecycle.onUpstreamWaitStart, lifecycle.onUpstreamWaitEnd, lifecycle.onIterationError);
149
161
  const transformed = transformSdkStream(wrapped.response, model, conversationId);
150
162
  const buffered = [];
163
+ if (lifecycle.bufferUntilComplete) {
164
+ try {
165
+ while (true) {
166
+ const item = await transformed.next();
167
+ if (item.done) {
168
+ await lifecycle.onComplete?.();
169
+ lifecycle.onTerminal?.();
170
+ return bufferedSseResponse(buffered);
171
+ }
172
+ buffered.push(encodeSseChunk(item.value));
173
+ }
174
+ }
175
+ catch (error) {
176
+ try {
177
+ await transformed.return(undefined);
178
+ }
179
+ catch { }
180
+ await wrapped.closeRaw();
181
+ throw error;
182
+ }
183
+ }
151
184
  let firstSemantic;
152
185
  while (true) {
153
186
  const item = await transformed.next();
154
187
  if (item.done) {
155
188
  await lifecycle.onComplete?.();
156
189
  lifecycle.onTerminal?.();
157
- let index = 0;
158
- return new Response(new ReadableStream({
159
- pull(controller) {
160
- const chunk = buffered[index++];
161
- if (chunk)
162
- controller.enqueue(chunk);
163
- if (index >= buffered.length)
164
- controller.close();
165
- }
166
- }, { highWaterMark: 0 }), { headers: { 'Content-Type': 'text/event-stream' } });
190
+ return bufferedSseResponse(buffered);
167
191
  }
168
192
  const encoded = encodeSseChunk(item.value);
169
193
  if (isSemanticChunk(item.value)) {
@@ -139,6 +139,8 @@ function applyEnvOverrides(config) {
139
139
  sdk_response_timeout_ms: parseNumberEnv(env.KIRO_SDK_RESPONSE_TIMEOUT_MS, config.sdk_response_timeout_ms),
140
140
  stream_event_timeout_enabled: parseBooleanEnv(env.KIRO_STREAM_EVENT_TIMEOUT_ENABLED, config.stream_event_timeout_enabled),
141
141
  request_timeout_ms: parseNumberEnv(env.KIRO_REQUEST_TIMEOUT_MS, config.request_timeout_ms),
142
+ stream_buffer_until_complete: parseBooleanEnv(env.KIRO_STREAM_BUFFER_UNTIL_COMPLETE, config.stream_buffer_until_complete),
143
+ stream_max_attempts: parseNumberEnv(env.KIRO_STREAM_MAX_ATTEMPTS, config.stream_max_attempts),
142
144
  token_expiry_buffer_ms: parseNumberEnv(env.KIRO_TOKEN_EXPIRY_BUFFER_MS, config.token_expiry_buffer_ms),
143
145
  usage_sync_max_retries: parseNumberEnv(env.KIRO_USAGE_SYNC_MAX_RETRIES, config.usage_sync_max_retries),
144
146
  auth_server_port_start: parseNumberEnv(env.KIRO_AUTH_SERVER_PORT_START, config.auth_server_port_start),
@@ -77,6 +77,17 @@ export declare const KiroConfigSchema: z.ZodObject<{
77
77
  * Only used when stream_event_timeout_enabled is true.
78
78
  */
79
79
  request_timeout_ms: z.ZodDefault<z.ZodNumber>;
80
+ /**
81
+ * Consume the complete Kiro event stream before exposing any semantic output
82
+ * downstream. This trades live token display for safe retries after a
83
+ * mid-stream transport failure without duplicating content or tool calls.
84
+ */
85
+ stream_buffer_until_complete: z.ZodDefault<z.ZodBoolean>;
86
+ /**
87
+ * Maximum number of complete event-stream attempts. Buffered mode can safely
88
+ * use every attempt even when the failed upstream stream produced output.
89
+ */
90
+ stream_max_attempts: z.ZodDefault<z.ZodNumber>;
80
91
  token_expiry_buffer_ms: z.ZodDefault<z.ZodNumber>;
81
92
  /**
82
93
  * Opt-in leader-elected keep-alive that proactively rotates idle-account
@@ -129,6 +140,8 @@ export declare const KiroConfigSchema: z.ZodObject<{
129
140
  sdk_response_timeout_ms: number;
130
141
  stream_event_timeout_enabled: boolean;
131
142
  request_timeout_ms: number;
143
+ stream_buffer_until_complete: boolean;
144
+ stream_max_attempts: number;
132
145
  token_expiry_buffer_ms: number;
133
146
  token_keepalive_enabled: boolean;
134
147
  token_keepalive_interval_ms: number;
@@ -165,6 +178,8 @@ export declare const KiroConfigSchema: z.ZodObject<{
165
178
  sdk_response_timeout_ms?: number | undefined;
166
179
  stream_event_timeout_enabled?: boolean | undefined;
167
180
  request_timeout_ms?: number | undefined;
181
+ stream_buffer_until_complete?: boolean | undefined;
182
+ stream_max_attempts?: number | undefined;
168
183
  token_expiry_buffer_ms?: number | undefined;
169
184
  token_keepalive_enabled?: boolean | undefined;
170
185
  token_keepalive_interval_ms?: number | undefined;
@@ -109,6 +109,17 @@ export const KiroConfigSchema = z.object({
109
109
  * Only used when stream_event_timeout_enabled is true.
110
110
  */
111
111
  request_timeout_ms: z.number().min(30000).max(600000).default(120000),
112
+ /**
113
+ * Consume the complete Kiro event stream before exposing any semantic output
114
+ * downstream. This trades live token display for safe retries after a
115
+ * mid-stream transport failure without duplicating content or tool calls.
116
+ */
117
+ stream_buffer_until_complete: z.boolean().default(false),
118
+ /**
119
+ * Maximum number of complete event-stream attempts. Buffered mode can safely
120
+ * use every attempt even when the failed upstream stream produced output.
121
+ */
122
+ stream_max_attempts: z.number().int().min(1).max(10).default(3),
112
123
  token_expiry_buffer_ms: z.number().min(30000).max(300000).default(300000),
113
124
  /**
114
125
  * Opt-in leader-elected keep-alive that proactively rotates idle-account
@@ -162,6 +173,8 @@ export const DEFAULT_CONFIG = {
162
173
  sdk_response_timeout_ms: 300000,
163
174
  stream_event_timeout_enabled: false,
164
175
  request_timeout_ms: 120000,
176
+ stream_buffer_until_complete: false,
177
+ stream_max_attempts: 3,
165
178
  token_expiry_buffer_ms: 300000,
166
179
  token_keepalive_enabled: false,
167
180
  token_keepalive_interval_ms: 600000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunerpy/opencode-kiro-auth",
3
- "version": "0.15.2",
3
+ "version": "0.15.3",
4
4
  "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",