opencode-timeout-continuer 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 harshpreet931
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # opencode-timeout-continuer
2
+
3
+ An OpenCode CLI plugin that automatically retries operations when timeout or retryable errors occur, using exponential backoff.
4
+
5
+ ## Features
6
+
7
+ - **Automatic retry** on timeout and retryable API errors
8
+ - **Exponential backoff** with configurable delays (1s → 2s → 4s → ...)
9
+ - **Per-session tracking** - retry counts reset when session completes
10
+ - **Silent operation** - no UI interruptions
11
+ - **Fully configurable** - customize retry count, delays, and prompts
12
+
13
+ ## Installation
14
+
15
+ ### From npm (when published)
16
+
17
+ ```bash
18
+ npm install -g opencode-timeout-continuer
19
+ ```
20
+
21
+ ### From source
22
+
23
+ ```bash
24
+ git clone <repo-url>
25
+ cd opencode-timeout-continuer
26
+ npm install
27
+ npm run build
28
+ npm pack
29
+ npm install -g opencode-timeout-continuer-*.tgz
30
+ ```
31
+
32
+ ## Configuration
33
+
34
+ Add the plugin to your `opencode.json`:
35
+
36
+ ```json
37
+ {
38
+ "plugin": ["opencode-timeout-continuer"],
39
+ "plugin_config": {
40
+ "opencode-timeout-continuer": {
41
+ "enabled": true,
42
+ "maxRetries": 3,
43
+ "baseDelayMs": 1000,
44
+ "maxDelayMs": 30000,
45
+ "prompt": "Continue"
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ ### Configuration Options
52
+
53
+ | Option | Type | Default | Description |
54
+ |--------|------|---------|-------------|
55
+ | `enabled` | boolean | `true` | Enable/disable auto-retry |
56
+ | `maxRetries` | number | `3` | Maximum retry attempts per session (1-10) |
57
+ | `baseDelayMs` | number | `1000` | Base delay for exponential backoff in ms (100-60000) |
58
+ | `maxDelayMs` | number | `30000` | Maximum delay cap in ms (1000-300000) |
59
+ | `prompt` | string | `"Continue"` | Prompt text sent on retry |
60
+
61
+ ## How It Works
62
+
63
+ 1. **Detect**: The plugin listens for `session.error` events from OpenCode
64
+ 2. **Analyze**: Checks if the error is retryable:
65
+ - API errors with `isRetryable: true`
66
+ - HTTP status codes: 408, 429, 502, 503, 504
67
+ - Error names containing "timeout" or "ETIMEDOUT"
68
+ 3. **Retry**: If retryable and under max retries:
69
+ - Calculates exponential backoff delay
70
+ - Sends a continuation prompt after the delay
71
+ 4. **Reset**: When a session goes idle, retry counts are reset
72
+
73
+ ### Exponential Backoff
74
+
75
+ The delay between retries grows exponentially:
76
+
77
+ | Attempt | Delay |
78
+ |---------|-------|
79
+ | 1 | 1s |
80
+ | 2 | 2s |
81
+ | 3 | 4s |
82
+ | 4 | 8s (capped at maxDelayMs) |
83
+
84
+ Formula: `delay = min(baseDelayMs * 2^attempt, maxDelayMs)`
85
+
86
+ ## Manual Testing
87
+
88
+ To test the plugin:
89
+
90
+ 1. Install the plugin globally
91
+ 2. Add it to your `opencode.json`
92
+ 3. Start OpenCode with a prompt that might timeout (e.g., a complex query)
93
+ 4. Watch for log messages in OpenCode's output:
94
+ - `Plugin initialized with maxRetries=3, baseDelayMs=1000`
95
+ - `Retryable error detected for session xxx, scheduling retry 1/3`
96
+
97
+ ## Error Types Handled
98
+
99
+ | Error Type | Retryable? |
100
+ |------------|------------|
101
+ | API Timeout | Yes |
102
+ | HTTP 408 (Request Timeout) | Yes |
103
+ | HTTP 429 (Too Many Requests) | Yes |
104
+ | HTTP 502 (Bad Gateway) | Yes |
105
+ | HTTP 503 (Service Unavailable) | Yes |
106
+ | HTTP 504 (Gateway Timeout) | Yes |
107
+ | ProviderAuthError | No |
108
+ | MessageOutputLengthError | No |
109
+ | MessageAbortedError | No |
110
+
111
+ ## License
112
+
113
+ MIT
@@ -0,0 +1,13 @@
1
+ import type { AutoContinueConfig } from './types';
2
+ /**
3
+ * Default configuration values
4
+ */
5
+ export declare const DEFAULT_CONFIG: AutoContinueConfig;
6
+ /**
7
+ * Gets the plugin configuration by merging user config with defaults
8
+ *
9
+ * @param pluginConfig - The plugin_config section from opencode.json
10
+ * @returns Merged and validated configuration
11
+ */
12
+ export declare function getConfig(pluginConfig?: Record<string, unknown>): AutoContinueConfig;
13
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,kBAM5B,CAAC;AAsCF;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,kBAAkB,CAYpF"}
package/dist/config.js ADDED
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_CONFIG = void 0;
4
+ exports.getConfig = getConfig;
5
+ /**
6
+ * Default configuration values
7
+ */
8
+ exports.DEFAULT_CONFIG = {
9
+ enabled: true,
10
+ maxRetries: 3,
11
+ baseDelayMs: 1000,
12
+ maxDelayMs: 30000,
13
+ prompt: 'Continue',
14
+ };
15
+ /**
16
+ * Validates a single config value and returns default if invalid
17
+ */
18
+ function validateNumber(value, defaultValue, min, max) {
19
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
20
+ return defaultValue;
21
+ }
22
+ if (min !== undefined && value < min) {
23
+ return defaultValue;
24
+ }
25
+ if (max !== undefined && value > max) {
26
+ return defaultValue;
27
+ }
28
+ return value;
29
+ }
30
+ /**
31
+ * Validates string config value
32
+ */
33
+ function validateString(value, defaultValue) {
34
+ if (typeof value !== 'string' || value.trim() === '') {
35
+ return defaultValue;
36
+ }
37
+ return value;
38
+ }
39
+ /**
40
+ * Validates boolean config value
41
+ */
42
+ function validateBoolean(value, defaultValue) {
43
+ if (typeof value !== 'boolean') {
44
+ return defaultValue;
45
+ }
46
+ return value;
47
+ }
48
+ /**
49
+ * Gets the plugin configuration by merging user config with defaults
50
+ *
51
+ * @param pluginConfig - The plugin_config section from opencode.json
52
+ * @returns Merged and validated configuration
53
+ */
54
+ function getConfig(pluginConfig) {
55
+ if (!pluginConfig || typeof pluginConfig !== 'object') {
56
+ return { ...exports.DEFAULT_CONFIG };
57
+ }
58
+ return {
59
+ enabled: validateBoolean(pluginConfig.enabled, exports.DEFAULT_CONFIG.enabled),
60
+ maxRetries: validateNumber(pluginConfig.maxRetries, exports.DEFAULT_CONFIG.maxRetries, 1, 10),
61
+ baseDelayMs: validateNumber(pluginConfig.baseDelayMs, exports.DEFAULT_CONFIG.baseDelayMs, 100, 60000),
62
+ maxDelayMs: validateNumber(pluginConfig.maxDelayMs, exports.DEFAULT_CONFIG.maxDelayMs, 1000, 300000),
63
+ prompt: validateString(pluginConfig.prompt, exports.DEFAULT_CONFIG.prompt),
64
+ };
65
+ }
66
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;AAuDA,8BAYC;AAjED;;GAEG;AACU,QAAA,cAAc,GAAuB;IAChD,OAAO,EAAE,IAAI;IACb,UAAU,EAAE,CAAC;IACb,WAAW,EAAE,IAAI;IACjB,UAAU,EAAE,KAAK;IACjB,MAAM,EAAE,UAAU;CACnB,CAAC;AAEF;;GAEG;AACH,SAAS,cAAc,CAAC,KAAc,EAAE,YAAoB,EAAE,GAAY,EAAE,GAAY;IACtF,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACzD,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,IAAI,GAAG,KAAK,SAAS,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;QACrC,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,IAAI,GAAG,KAAK,SAAS,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;QACrC,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,SAAS,cAAc,CAAC,KAAc,EAAE,YAAoB;IAC1D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,SAAS,eAAe,CAAC,KAAc,EAAE,YAAqB;IAC5D,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,YAAY,CAAC;IACtB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;GAKG;AACH,SAAgB,SAAS,CAAC,YAAsC;IAC9D,IAAI,CAAC,YAAY,IAAI,OAAO,YAAY,KAAK,QAAQ,EAAE,CAAC;QACtD,OAAO,EAAE,GAAG,sBAAc,EAAE,CAAC;IAC/B,CAAC;IAED,OAAO;QACL,OAAO,EAAE,eAAe,CAAC,YAAY,CAAC,OAAO,EAAE,sBAAc,CAAC,OAAO,CAAC;QACtE,UAAU,EAAE,cAAc,CAAC,YAAY,CAAC,UAAU,EAAE,sBAAc,CAAC,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC;QACrF,WAAW,EAAE,cAAc,CAAC,YAAY,CAAC,WAAW,EAAE,sBAAc,CAAC,WAAW,EAAE,GAAG,EAAE,KAAK,CAAC;QAC7F,UAAU,EAAE,cAAc,CAAC,YAAY,CAAC,UAAU,EAAE,sBAAc,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC;QAC5F,MAAM,EAAE,cAAc,CAAC,YAAY,CAAC,MAAM,EAAE,sBAAc,CAAC,MAAM,CAAC;KACnE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,57 @@
1
+ import type { OpenCodeEvent } from './types.js';
2
+ /**
3
+ * OpenCode plugin for automatic retry on timeout errors
4
+ *
5
+ * This plugin automatically retries OpenCode operations when timeout
6
+ * or retryable errors occur, using exponential backoff.
7
+ *
8
+ * Configuration (in opencode.json):
9
+ * ```json
10
+ * {
11
+ * "plugin": ["opencode-timeout-continuer"],
12
+ * "plugin_config": {
13
+ * "opencode-timeout-continuer": {
14
+ * "enabled": true,
15
+ * "maxRetries": 3,
16
+ * "baseDelayMs": 1000,
17
+ * "maxDelayMs": 30000,
18
+ * "prompt": "Continue"
19
+ * }
20
+ * }
21
+ * }
22
+ * ```
23
+ */
24
+ type Plugin = (input: {
25
+ client: {
26
+ session: {
27
+ prompt: (params: {
28
+ path: {
29
+ id: string;
30
+ };
31
+ body: {
32
+ parts: Array<{
33
+ type: string;
34
+ text: string;
35
+ }>;
36
+ };
37
+ }) => Promise<void>;
38
+ };
39
+ app: {
40
+ log: (params: {
41
+ body: {
42
+ service: string;
43
+ level: 'debug' | 'info' | 'warn' | 'error';
44
+ message: string;
45
+ };
46
+ }) => Promise<void>;
47
+ };
48
+ };
49
+ config?: Record<string, unknown>;
50
+ }) => Promise<{
51
+ event?: (params: {
52
+ event: OpenCodeEvent;
53
+ }) => Promise<void>;
54
+ }>;
55
+ declare const plugin: Plugin;
56
+ export default plugin;
57
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAA+B,MAAM,YAAY,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,KAAK,MAAM,GAAG,CAAC,KAAK,EAAE;IACpB,MAAM,EAAE;QACN,OAAO,EAAE;YACP,MAAM,EAAE,CAAC,MAAM,EAAE;gBACf,IAAI,EAAE;oBAAE,EAAE,EAAE,MAAM,CAAA;iBAAE,CAAC;gBACrB,IAAI,EAAE;oBAAE,KAAK,EAAE,KAAK,CAAC;wBAAE,IAAI,EAAE,MAAM,CAAC;wBAAC,IAAI,EAAE,MAAM,CAAA;qBAAE,CAAC,CAAA;iBAAE,CAAC;aACxD,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;SACrB,CAAC;QACF,GAAG,EAAE;YACH,GAAG,EAAE,CAAC,MAAM,EAAE;gBACZ,IAAI,EAAE;oBACJ,OAAO,EAAE,MAAM,CAAC;oBAChB,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;oBAC3C,OAAO,EAAE,MAAM,CAAC;iBACjB,CAAC;aACH,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;SACrB,CAAC;KACH,CAAC;IACF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC,KAAK,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,aAAa,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D,CAAC,CAAC;AAEH,QAAA,MAAM,MAAM,EAAE,MA2Fb,CAAC;AAEF,eAAe,MAAM,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const retry_manager_js_1 = require("./retry-manager.js");
4
+ const config_js_1 = require("./config.js");
5
+ const plugin = async ({ client, config }) => {
6
+ // Get plugin configuration
7
+ const pluginConfig = config;
8
+ const retryConfig = (0, config_js_1.getConfig)(pluginConfig);
9
+ // Initialize retry manager
10
+ const retryManager = new retry_manager_js_1.RetryManager(retryConfig);
11
+ // Log plugin initialization
12
+ await client.app.log({
13
+ body: {
14
+ service: 'opencode-timeout-continuer',
15
+ level: 'info',
16
+ message: `Plugin initialized with maxRetries=${retryConfig.maxRetries}, baseDelayMs=${retryConfig.baseDelayMs}`,
17
+ },
18
+ });
19
+ return {
20
+ event: async ({ event }) => {
21
+ // Handle session.error
22
+ if (event.type === 'session.error') {
23
+ const { sessionID, error } = event.properties;
24
+ if (!sessionID) {
25
+ return;
26
+ }
27
+ // Check if we should retry
28
+ if (retryManager.shouldRetry(sessionID, error)) {
29
+ const currentCount = retryManager.getCount(sessionID);
30
+ await client.app.log({
31
+ body: {
32
+ service: 'opencode-timeout-continuer',
33
+ level: 'info',
34
+ message: `Retryable error detected for session ${sessionID}, scheduling retry ${currentCount + 1}/${retryConfig.maxRetries}`,
35
+ },
36
+ });
37
+ // Schedule retry with exponential backoff
38
+ retryManager.scheduleRetry(sessionID, async () => {
39
+ try {
40
+ await client.session.prompt({
41
+ path: { id: sessionID },
42
+ body: {
43
+ parts: [{ type: 'text', text: retryConfig.prompt }],
44
+ },
45
+ });
46
+ }
47
+ catch (err) {
48
+ await client.app.log({
49
+ body: {
50
+ service: 'opencode-timeout-continuer',
51
+ level: 'error',
52
+ message: `Failed to send retry prompt: ${err instanceof Error ? err.message : String(err)}`,
53
+ },
54
+ });
55
+ }
56
+ }, currentCount);
57
+ }
58
+ else if (error) {
59
+ // Log non-retryable error for debugging
60
+ const errInfo = error;
61
+ await client.app.log({
62
+ body: {
63
+ service: 'opencode-timeout-continuer',
64
+ level: 'debug',
65
+ message: `Non-retryable error for session ${sessionID}: ${errInfo.name || 'unknown'} - ${errInfo.data?.message || 'no message'}`,
66
+ },
67
+ });
68
+ }
69
+ }
70
+ // Handle session.idle - reset retry state
71
+ if (event.type === 'session.idle') {
72
+ const { sessionID } = event.properties;
73
+ if (sessionID) {
74
+ retryManager.reset(sessionID);
75
+ await client.app.log({
76
+ body: {
77
+ service: 'opencode-timeout-continuer',
78
+ level: 'debug',
79
+ message: `Session ${sessionID} went idle, reset retry state`,
80
+ },
81
+ });
82
+ }
83
+ }
84
+ },
85
+ };
86
+ };
87
+ exports.default = plugin;
88
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { AutoContinueConfig } from './types';
2
+ /**
3
+ * Manages retry state and exponential backoff for session errors
4
+ */
5
+ export declare class RetryManager {
6
+ private sessions;
7
+ private config;
8
+ constructor(config: AutoContinueConfig);
9
+ /**
10
+ * Check if an error is retryable
11
+ */
12
+ private isRetryableError;
13
+ /**
14
+ * Check if we should retry for this session
15
+ */
16
+ shouldRetry(sessionId: string, error: unknown): boolean;
17
+ /**
18
+ * Get current retry count for a session
19
+ */
20
+ getCount(sessionId: string): number;
21
+ /**
22
+ * Schedule a retry with exponential backoff
23
+ */
24
+ scheduleRetry(sessionId: string, callback: () => void | Promise<void>, attempt: number): void;
25
+ /**
26
+ * Reset retry state for a session (called on session.idle)
27
+ */
28
+ reset(sessionId: string): void;
29
+ /**
30
+ * Clear all retry state (cleanup on plugin unload)
31
+ */
32
+ clearAll(): void;
33
+ }
34
+ //# sourceMappingURL=retry-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-manager.d.ts","sourceRoot":"","sources":["../src/retry-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAqB,MAAM,SAAS,CAAC;AAErE;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAwC;IACxD,OAAO,CAAC,MAAM,CAAqB;gBAEvB,MAAM,EAAE,kBAAkB;IAItC;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA2BxB;;OAEG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO;IAmBvD;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAInC;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IA2B7F;;OAEG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAQ9B;;OAEG;IACH,QAAQ,IAAI,IAAI;CAQjB"}
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RetryManager = void 0;
4
+ /**
5
+ * Manages retry state and exponential backoff for session errors
6
+ */
7
+ class RetryManager {
8
+ sessions = new Map();
9
+ config;
10
+ constructor(config) {
11
+ this.config = config;
12
+ }
13
+ /**
14
+ * Check if an error is retryable
15
+ */
16
+ isRetryableError(error) {
17
+ if (!error || typeof error !== 'object') {
18
+ return false;
19
+ }
20
+ const err = error;
21
+ // Check for ApiError with isRetryable flag
22
+ if (err.name === 'ApiError' && err.data?.isRetryable === true) {
23
+ return true;
24
+ }
25
+ // Check for timeout-related status codes
26
+ const statusCode = err.data?.statusCode;
27
+ if (statusCode && [408, 429, 502, 503, 504].includes(statusCode)) {
28
+ return true;
29
+ }
30
+ // Check error name for timeout patterns
31
+ const errorName = err.name?.toLowerCase() || '';
32
+ if (errorName.includes('timeout') || errorName.includes('etimedout')) {
33
+ return true;
34
+ }
35
+ return false;
36
+ }
37
+ /**
38
+ * Check if we should retry for this session
39
+ */
40
+ shouldRetry(sessionId, error) {
41
+ // Check if plugin is enabled
42
+ if (!this.config.enabled) {
43
+ return false;
44
+ }
45
+ // Check if error is retryable
46
+ if (!this.isRetryableError(error)) {
47
+ return false;
48
+ }
49
+ // Get or create session state
50
+ const state = this.sessions.get(sessionId);
51
+ const currentCount = state?.count ?? 0;
52
+ // Check retry limit
53
+ return currentCount < this.config.maxRetries;
54
+ }
55
+ /**
56
+ * Get current retry count for a session
57
+ */
58
+ getCount(sessionId) {
59
+ return this.sessions.get(sessionId)?.count ?? 0;
60
+ }
61
+ /**
62
+ * Schedule a retry with exponential backoff
63
+ */
64
+ scheduleRetry(sessionId, callback, attempt) {
65
+ // Calculate delay with exponential backoff
66
+ const delay = Math.min(this.config.baseDelayMs * Math.pow(2, attempt), this.config.maxDelayMs);
67
+ // Increment retry count
68
+ const currentState = this.sessions.get(sessionId);
69
+ const newState = {
70
+ count: (currentState?.count ?? 0) + 1,
71
+ lastError: new Date(),
72
+ };
73
+ // Set up timeout
74
+ const timeoutId = setTimeout(async () => {
75
+ try {
76
+ await callback();
77
+ }
78
+ catch {
79
+ // Callback errors are handled by the event system
80
+ }
81
+ }, delay);
82
+ newState.timeoutId = timeoutId;
83
+ this.sessions.set(sessionId, newState);
84
+ }
85
+ /**
86
+ * Reset retry state for a session (called on session.idle)
87
+ */
88
+ reset(sessionId) {
89
+ const state = this.sessions.get(sessionId);
90
+ if (state?.timeoutId) {
91
+ clearTimeout(state.timeoutId);
92
+ }
93
+ this.sessions.delete(sessionId);
94
+ }
95
+ /**
96
+ * Clear all retry state (cleanup on plugin unload)
97
+ */
98
+ clearAll() {
99
+ for (const state of this.sessions.values()) {
100
+ if (state.timeoutId) {
101
+ clearTimeout(state.timeoutId);
102
+ }
103
+ }
104
+ this.sessions.clear();
105
+ }
106
+ }
107
+ exports.RetryManager = RetryManager;
108
+ //# sourceMappingURL=retry-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-manager.js","sourceRoot":"","sources":["../src/retry-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Configuration options for the auto-continue plugin
3
+ */
4
+ export interface AutoContinueConfig {
5
+ /** Enable/disable auto-continue functionality */
6
+ enabled: boolean;
7
+ /** Maximum number of retry attempts */
8
+ maxRetries: number;
9
+ /** Base delay in milliseconds for exponential backoff */
10
+ baseDelayMs: number;
11
+ /** Maximum delay cap in milliseconds */
12
+ maxDelayMs: number;
13
+ /** Prompt text to send when retrying */
14
+ prompt: string;
15
+ }
16
+ /**
17
+ * Tracks retry state for a specific session
18
+ */
19
+ export interface SessionRetryState {
20
+ /** Current retry count */
21
+ count: number;
22
+ /** Timestamp of last error */
23
+ lastError: Date;
24
+ /** Pending timeout ID for cleanup */
25
+ timeoutId?: ReturnType<typeof setTimeout>;
26
+ }
27
+ /**
28
+ * API error structure from OpenCode
29
+ */
30
+ export interface ApiError {
31
+ name: 'ApiError';
32
+ data: {
33
+ message: string;
34
+ statusCode?: number;
35
+ isRetryable: boolean;
36
+ responseHeaders?: Record<string, string>;
37
+ responseBody?: string;
38
+ };
39
+ }
40
+ /**
41
+ * Other error types that should NOT be retried
42
+ */
43
+ export type NonRetryableError = {
44
+ name: 'ProviderAuthError';
45
+ } | {
46
+ name: 'MessageOutputLengthError';
47
+ } | {
48
+ name: 'MessageAbortedError';
49
+ };
50
+ /**
51
+ * Event properties for session.error
52
+ */
53
+ export interface SessionErrorEventProperties {
54
+ sessionID?: string;
55
+ error?: ApiError | NonRetryableError | Error;
56
+ }
57
+ /**
58
+ * Event properties for session.idle
59
+ */
60
+ export interface SessionIdleEventProperties {
61
+ sessionID: string;
62
+ }
63
+ /**
64
+ * OpenCode event types that the plugin handles
65
+ */
66
+ export type OpenCodeEvent = {
67
+ type: 'session.error';
68
+ properties: SessionErrorEventProperties;
69
+ } | {
70
+ type: 'session.idle';
71
+ properties: SessionIdleEventProperties;
72
+ };
73
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,uCAAuC;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,SAAS,EAAE,IAAI,CAAC;IAChB,qCAAqC;IACrC,SAAS,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;CAC3C;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE;QACJ,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,OAAO,CAAC;QACrB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACzC,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,mBAAmB,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,0BAA0B,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,qBAAqB,CAAA;CAAE,CAAC;AAEpC;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,QAAQ,GAAG,iBAAiB,GAAG,KAAK,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,UAAU,EAAE,2BAA2B,CAAA;CAAE,GAClE;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,UAAU,EAAE,0BAA0B,CAAA;CAAE,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "opencode-timeout-continuer",
3
+ "version": "1.0.0",
4
+ "description": "OpenCode CLI plugin for automatic retry on timeout errors with exponential backoff",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "opencode",
19
+ "plugin",
20
+ "timeout",
21
+ "retry",
22
+ "exponential-backoff"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/harshpreet931/opencode-timeout-continuer.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/harshpreet931/opencode-timeout-continuer/issues"
30
+ },
31
+ "homepage": "https://github.com/harshpreet931/opencode-timeout-continuer#readme",
32
+ "author": "harshpreet931",
33
+ "license": "MIT",
34
+ "files": [
35
+ "dist",
36
+ "README.md"
37
+ ],
38
+ "devDependencies": {
39
+ "typescript": "^5.3.0"
40
+ },
41
+ "peerDependencies": {},
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ }
45
+ }