@otto-assistant/otto 0.7.15 → 0.7.16
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/dist/commands/discord-commands-group-a.test.js +34 -0
- package/dist/commands/login.js +8 -2
- package/dist/commands/model-variant.js +4 -1
- package/dist/commands/model.js +7 -3
- package/dist/discord-bot.js +2 -2
- package/dist/errors.js +22 -0
- package/dist/opencode.js +8 -1
- package/package.json +3 -3
- package/src/commands/discord-commands-group-a.test.ts +38 -0
- package/src/commands/login.ts +8 -2
- package/src/commands/model-variant.ts +4 -1
- package/src/commands/model.ts +7 -3
- package/src/discord-bot.ts +4 -2
- package/src/errors.ts +31 -0
- package/src/opencode.ts +9 -0
|
@@ -429,6 +429,40 @@ describe('/model', () => {
|
|
|
429
429
|
Select a provider:"
|
|
430
430
|
`);
|
|
431
431
|
});
|
|
432
|
+
test('shows provider list error details', async () => {
|
|
433
|
+
const textChannel = { id: 'channel-1', type: ChannelType.GuildText };
|
|
434
|
+
const { command } = createCommand({ channel: textChannel });
|
|
435
|
+
mockGetOttoMetadata.mockResolvedValue({ projectDirectory: '/repo' });
|
|
436
|
+
mockInitializeOpencodeForDirectory.mockResolvedValue(() => {
|
|
437
|
+
return {
|
|
438
|
+
provider: {
|
|
439
|
+
list: vi.fn(async () => {
|
|
440
|
+
return {
|
|
441
|
+
error: {
|
|
442
|
+
name: 'UnknownError',
|
|
443
|
+
data: {
|
|
444
|
+
message: 'Cursor token refresh failed: {"code":"internal","message":"Error"}',
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}),
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
await handleModelCommand({
|
|
453
|
+
interaction: command,
|
|
454
|
+
appId: 'app-1',
|
|
455
|
+
});
|
|
456
|
+
expect(command.editReply.mock.calls).toMatchInlineSnapshot(`
|
|
457
|
+
[
|
|
458
|
+
[
|
|
459
|
+
{
|
|
460
|
+
"content": "Failed to fetch providers: Cursor token refresh failed: {"code":"internal","message":"Error"}",
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
]
|
|
464
|
+
`);
|
|
465
|
+
});
|
|
432
466
|
});
|
|
433
467
|
describe('/agent', () => {
|
|
434
468
|
test('shows selectable agents and current channel override', async () => {
|
package/dist/commands/login.js
CHANGED
|
@@ -13,6 +13,7 @@ import crypto from 'node:crypto';
|
|
|
13
13
|
import { initializeOpencodeForDirectory, getOpencodeServerPort, } from '../opencode.js';
|
|
14
14
|
import { resolveTextChannel, getOttoMetadata } from '../discord-utils.js';
|
|
15
15
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
16
|
+
import { formatOpenCodeResponseError } from '../errors.js';
|
|
16
17
|
import { buildPaginatedOptions, parsePaginationValue } from './paginated-select.js';
|
|
17
18
|
const loginLogger = createLogger(LogPrefix.LOGIN);
|
|
18
19
|
// ── Context store ───────────────────────────────────────────────
|
|
@@ -141,7 +142,9 @@ export async function handleLoginCommand({ interaction, }) {
|
|
|
141
142
|
directory: projectDirectory,
|
|
142
143
|
});
|
|
143
144
|
if (!providersResponse.data) {
|
|
144
|
-
await interaction.editReply({
|
|
145
|
+
await interaction.editReply({
|
|
146
|
+
content: `Failed to fetch providers: ${formatOpenCodeResponseError(providersResponse.error)}`,
|
|
147
|
+
});
|
|
145
148
|
return;
|
|
146
149
|
}
|
|
147
150
|
const { all: allProviders, connected } = providersResponse.data;
|
|
@@ -272,7 +275,10 @@ async function handleProviderStep(interaction, ctx, hash, providerId) {
|
|
|
272
275
|
}
|
|
273
276
|
const providersResponse = await getClient().provider.list({ directory: ctx.dir });
|
|
274
277
|
if (!providersResponse.data) {
|
|
275
|
-
await interaction.editReply({
|
|
278
|
+
await interaction.editReply({
|
|
279
|
+
content: `Failed to fetch providers: ${formatOpenCodeResponseError(providersResponse.error)}`,
|
|
280
|
+
components: [],
|
|
281
|
+
});
|
|
276
282
|
return;
|
|
277
283
|
}
|
|
278
284
|
const { all: allProviders, connected } = providersResponse.data;
|
|
@@ -14,6 +14,7 @@ import { getCurrentModelInfo, ensureSessionPreferencesSnapshot, } from './model.
|
|
|
14
14
|
import { getRuntime } from '../session-handler/thread-session-runtime.js';
|
|
15
15
|
import { getThinkingValuesForModel } from '../thinking-utils.js';
|
|
16
16
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
17
|
+
import { formatOpenCodeResponseError } from '../errors.js';
|
|
17
18
|
const logger = createLogger(LogPrefix.MODEL);
|
|
18
19
|
const pendingVariantContexts = new Map();
|
|
19
20
|
/** 10 minute TTL for pending contexts to prevent unbounded map growth */
|
|
@@ -121,7 +122,9 @@ export async function handleModelVariantCommand({ interaction, appId, }) {
|
|
|
121
122
|
return;
|
|
122
123
|
}
|
|
123
124
|
if (!providersResponse.data) {
|
|
124
|
-
await interaction.editReply({
|
|
125
|
+
await interaction.editReply({
|
|
126
|
+
content: `Failed to fetch providers: ${formatOpenCodeResponseError(providersResponse.error)}`,
|
|
127
|
+
});
|
|
125
128
|
return;
|
|
126
129
|
}
|
|
127
130
|
const { providerID, modelID, model: fullModelId } = currentModelInfo;
|
package/dist/commands/model.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getDefaultModel } from '../session-handler/model-utils.js';
|
|
|
8
8
|
import { getRuntime } from '../session-handler/thread-session-runtime.js';
|
|
9
9
|
import { getThinkingValuesForModel } from '../thinking-utils.js';
|
|
10
10
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
11
|
+
import { formatOpenCodeResponseError } from '../errors.js';
|
|
11
12
|
import * as errore from 'errore';
|
|
12
13
|
import { buildPaginatedOptions, parsePaginationValue } from './paginated-select.js';
|
|
13
14
|
const modelLogger = createLogger(LogPrefix.MODEL);
|
|
@@ -270,7 +271,7 @@ export async function handleModelCommand({ interaction, appId, }) {
|
|
|
270
271
|
]);
|
|
271
272
|
if (!providersResponse.data) {
|
|
272
273
|
await interaction.editReply({
|
|
273
|
-
content:
|
|
274
|
+
content: `Failed to fetch providers: ${formatOpenCodeResponseError(providersResponse.error)}`,
|
|
274
275
|
});
|
|
275
276
|
return;
|
|
276
277
|
}
|
|
@@ -393,7 +394,10 @@ export async function handleProviderSelectMenu(interaction) {
|
|
|
393
394
|
}
|
|
394
395
|
const providersResponse = await getClient().provider.list({ directory: context.dir });
|
|
395
396
|
if (!providersResponse.data) {
|
|
396
|
-
await interaction.editReply({
|
|
397
|
+
await interaction.editReply({
|
|
398
|
+
content: `Failed to fetch providers: ${formatOpenCodeResponseError(providersResponse.error)}`,
|
|
399
|
+
components: [],
|
|
400
|
+
});
|
|
397
401
|
return;
|
|
398
402
|
}
|
|
399
403
|
const { all: allProviders, connected } = providersResponse.data;
|
|
@@ -434,7 +438,7 @@ export async function handleProviderSelectMenu(interaction) {
|
|
|
434
438
|
});
|
|
435
439
|
if (!providersResponse.data) {
|
|
436
440
|
await interaction.editReply({
|
|
437
|
-
content:
|
|
441
|
+
content: `Failed to fetch providers: ${formatOpenCodeResponseError(providersResponse.error)}`,
|
|
438
442
|
components: [],
|
|
439
443
|
});
|
|
440
444
|
return;
|
package/dist/discord-bot.js
CHANGED
|
@@ -43,11 +43,11 @@ import * as errore from "errore";
|
|
|
43
43
|
import { createLogger, formatErrorWithStack, LogPrefix } from "./logger.js";
|
|
44
44
|
import { writeHeapSnapshot, startHeapMonitor } from "./heap-monitor.js";
|
|
45
45
|
import { startTaskRunner } from "./task-runner.js";
|
|
46
|
+
import { setGlobalDispatcher, Agent } from "undici";
|
|
46
47
|
// Increase connection pool to prevent deadlock when multiple sessions have open SSE streams.
|
|
47
48
|
// Each session's event.subscribe() holds a connection; without enough connections,
|
|
48
49
|
// regular HTTP requests (question.reply, session.prompt) get blocked → deadlock.
|
|
49
|
-
|
|
50
|
-
// Types are declared in src/undici.d.ts.
|
|
50
|
+
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0, connections: 500 }));
|
|
51
51
|
const discordLogger = createLogger(LogPrefix.DISCORD);
|
|
52
52
|
const voiceLogger = createLogger(LogPrefix.VOICE);
|
|
53
53
|
// Well-known WebSocket and Discord Gateway close codes for diagnostic logging.
|
package/dist/errors.js
CHANGED
|
@@ -155,3 +155,25 @@ export class GitCommandError extends createTaggedError({
|
|
|
155
155
|
message: 'Git command failed: $command',
|
|
156
156
|
}) {
|
|
157
157
|
}
|
|
158
|
+
export function formatOpenCodeResponseError(error) {
|
|
159
|
+
if (error && typeof error === 'object') {
|
|
160
|
+
if ('data' in error &&
|
|
161
|
+
error.data &&
|
|
162
|
+
typeof error.data === 'object' &&
|
|
163
|
+
'message' in error.data) {
|
|
164
|
+
return String(error.data.message);
|
|
165
|
+
}
|
|
166
|
+
if ('errors' in error &&
|
|
167
|
+
Array.isArray(error.errors) &&
|
|
168
|
+
error.errors.length > 0) {
|
|
169
|
+
return JSON.stringify(error.errors);
|
|
170
|
+
}
|
|
171
|
+
if ('message' in error && typeof error.message === 'string') {
|
|
172
|
+
return error.message;
|
|
173
|
+
}
|
|
174
|
+
if ('name' in error && typeof error.name === 'string') {
|
|
175
|
+
return error.name;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return 'Unknown OpenCode API error';
|
|
179
|
+
}
|
package/dist/opencode.js
CHANGED
|
@@ -39,7 +39,7 @@ const STARTUP_STDERR_TAIL_LIMIT = 30;
|
|
|
39
39
|
const STARTUP_STDERR_LINE_MAX_LENGTH = 120;
|
|
40
40
|
const STARTUP_ERROR_REASON_MAX_LENGTH = 1500;
|
|
41
41
|
const ANSI_ESCAPE_REGEX = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
|
|
42
|
-
async function requestHealthcheck({ url, }) {
|
|
42
|
+
async function requestHealthcheck({ url, timeoutMs = 2000, }) {
|
|
43
43
|
return new Promise((resolve, reject) => {
|
|
44
44
|
const req = http.request(url, {
|
|
45
45
|
method: 'GET',
|
|
@@ -58,6 +58,13 @@ async function requestHealthcheck({ url, }) {
|
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
60
|
});
|
|
61
|
+
// Without a timeout, a stalled connection (TCP accepted during opencode
|
|
62
|
+
// startup before the HTTP layer is ready) hangs forever, wedging
|
|
63
|
+
// waitForServer on its first iteration and blocking the entire single
|
|
64
|
+
// server startup promise. Abort and let the poll loop retry instead.
|
|
65
|
+
req.setTimeout(timeoutMs, () => {
|
|
66
|
+
req.destroy(new Error(`healthcheck timed out after ${timeoutMs}ms`));
|
|
67
|
+
});
|
|
61
68
|
req.on('error', reject);
|
|
62
69
|
req.end();
|
|
63
70
|
});
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"module": "index.ts",
|
|
9
9
|
"type": "module",
|
|
10
|
-
"version": "0.7.
|
|
10
|
+
"version": "0.7.16",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"dev": "tsx src/bin.ts",
|
|
13
13
|
"prepublishOnly": "pnpm build",
|
|
@@ -52,8 +52,7 @@
|
|
|
52
52
|
"opencode-cached-provider": "workspace:^",
|
|
53
53
|
"opencode-deterministic-provider": "workspace:^",
|
|
54
54
|
"prisma": "7.4.2",
|
|
55
|
-
"tsx": "^4.20.5"
|
|
56
|
-
"undici": "^8.0.2"
|
|
55
|
+
"tsx": "^4.20.5"
|
|
57
56
|
},
|
|
58
57
|
"dependencies": {
|
|
59
58
|
"@ai-sdk/google": "^3.0.53",
|
|
@@ -88,6 +87,7 @@
|
|
|
88
87
|
"proper-lockfile": "^4.1.2",
|
|
89
88
|
"string-dedent": "^3.0.2",
|
|
90
89
|
"traforo": "^0.4.0",
|
|
90
|
+
"undici": "^8.0.2",
|
|
91
91
|
"ws": "^8.19.0",
|
|
92
92
|
"xdg-basedir": "^5.1.0",
|
|
93
93
|
"yaml": "^2.8.3",
|
|
@@ -512,6 +512,44 @@ describe('/model', () => {
|
|
|
512
512
|
`,
|
|
513
513
|
)
|
|
514
514
|
})
|
|
515
|
+
|
|
516
|
+
test('shows provider list error details', async () => {
|
|
517
|
+
const textChannel = { id: 'channel-1', type: ChannelType.GuildText }
|
|
518
|
+
const { command } = createCommand({ channel: textChannel })
|
|
519
|
+
|
|
520
|
+
mockGetOttoMetadata.mockResolvedValue({ projectDirectory: '/repo' })
|
|
521
|
+
mockInitializeOpencodeForDirectory.mockResolvedValue(() => {
|
|
522
|
+
return {
|
|
523
|
+
provider: {
|
|
524
|
+
list: vi.fn(async () => {
|
|
525
|
+
return {
|
|
526
|
+
error: {
|
|
527
|
+
name: 'UnknownError',
|
|
528
|
+
data: {
|
|
529
|
+
message: 'Cursor token refresh failed: {"code":"internal","message":"Error"}',
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
}
|
|
533
|
+
}),
|
|
534
|
+
},
|
|
535
|
+
}
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
await handleModelCommand({
|
|
539
|
+
interaction: command as never,
|
|
540
|
+
appId: 'app-1',
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
expect(command.editReply.mock.calls).toMatchInlineSnapshot(`
|
|
544
|
+
[
|
|
545
|
+
[
|
|
546
|
+
{
|
|
547
|
+
"content": "Failed to fetch providers: Cursor token refresh failed: {"code":"internal","message":"Error"}",
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
]
|
|
551
|
+
`)
|
|
552
|
+
})
|
|
515
553
|
})
|
|
516
554
|
|
|
517
555
|
describe('/agent', () => {
|
package/src/commands/login.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from '../opencode.js'
|
|
35
35
|
import { resolveTextChannel, getOttoMetadata } from '../discord-utils.js'
|
|
36
36
|
import { createLogger, LogPrefix } from '../logger.js'
|
|
37
|
+
import { formatOpenCodeResponseError } from '../errors.js'
|
|
37
38
|
import { buildPaginatedOptions, parsePaginationValue } from './paginated-select.js'
|
|
38
39
|
|
|
39
40
|
const loginLogger = createLogger(LogPrefix.LOGIN)
|
|
@@ -266,7 +267,9 @@ export async function handleLoginCommand({
|
|
|
266
267
|
})
|
|
267
268
|
|
|
268
269
|
if (!providersResponse.data) {
|
|
269
|
-
await interaction.editReply({
|
|
270
|
+
await interaction.editReply({
|
|
271
|
+
content: `Failed to fetch providers: ${formatOpenCodeResponseError(providersResponse.error)}`,
|
|
272
|
+
})
|
|
270
273
|
return
|
|
271
274
|
}
|
|
272
275
|
|
|
@@ -416,7 +419,10 @@ async function handleProviderStep(
|
|
|
416
419
|
}
|
|
417
420
|
const providersResponse = await getClient().provider.list({ directory: ctx.dir })
|
|
418
421
|
if (!providersResponse.data) {
|
|
419
|
-
await interaction.editReply({
|
|
422
|
+
await interaction.editReply({
|
|
423
|
+
content: `Failed to fetch providers: ${formatOpenCodeResponseError(providersResponse.error)}`,
|
|
424
|
+
components: [],
|
|
425
|
+
})
|
|
420
426
|
return
|
|
421
427
|
}
|
|
422
428
|
const { all: allProviders, connected } = providersResponse.data
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { getRuntime } from '../session-handler/thread-session-runtime.js'
|
|
35
35
|
import { getThinkingValuesForModel } from '../thinking-utils.js'
|
|
36
36
|
import { createLogger, LogPrefix } from '../logger.js'
|
|
37
|
+
import { formatOpenCodeResponseError } from '../errors.js'
|
|
37
38
|
|
|
38
39
|
const logger = createLogger(LogPrefix.MODEL)
|
|
39
40
|
|
|
@@ -184,7 +185,9 @@ export async function handleModelVariantCommand({
|
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
if (!providersResponse.data) {
|
|
187
|
-
await interaction.editReply({
|
|
188
|
+
await interaction.editReply({
|
|
189
|
+
content: `Failed to fetch providers: ${formatOpenCodeResponseError(providersResponse.error)}`,
|
|
190
|
+
})
|
|
188
191
|
return
|
|
189
192
|
}
|
|
190
193
|
|
package/src/commands/model.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { getDefaultModel } from '../session-handler/model-utils.js'
|
|
|
30
30
|
import { getRuntime } from '../session-handler/thread-session-runtime.js'
|
|
31
31
|
import { getThinkingValuesForModel } from '../thinking-utils.js'
|
|
32
32
|
import { createLogger, LogPrefix } from '../logger.js'
|
|
33
|
+
import { formatOpenCodeResponseError } from '../errors.js'
|
|
33
34
|
import * as errore from 'errore'
|
|
34
35
|
import { buildPaginatedOptions, parsePaginationValue } from './paginated-select.js'
|
|
35
36
|
|
|
@@ -447,7 +448,7 @@ export async function handleModelCommand({
|
|
|
447
448
|
|
|
448
449
|
if (!providersResponse.data) {
|
|
449
450
|
await interaction.editReply({
|
|
450
|
-
content:
|
|
451
|
+
content: `Failed to fetch providers: ${formatOpenCodeResponseError(providersResponse.error)}`,
|
|
451
452
|
})
|
|
452
453
|
return
|
|
453
454
|
}
|
|
@@ -596,7 +597,10 @@ export async function handleProviderSelectMenu(
|
|
|
596
597
|
}
|
|
597
598
|
const providersResponse = await getClient().provider.list({ directory: context.dir })
|
|
598
599
|
if (!providersResponse.data) {
|
|
599
|
-
await interaction.editReply({
|
|
600
|
+
await interaction.editReply({
|
|
601
|
+
content: `Failed to fetch providers: ${formatOpenCodeResponseError(providersResponse.error)}`,
|
|
602
|
+
components: [],
|
|
603
|
+
})
|
|
600
604
|
return
|
|
601
605
|
}
|
|
602
606
|
const { all: allProviders, connected } = providersResponse.data
|
|
@@ -640,7 +644,7 @@ export async function handleProviderSelectMenu(
|
|
|
640
644
|
|
|
641
645
|
if (!providersResponse.data) {
|
|
642
646
|
await interaction.editReply({
|
|
643
|
-
content:
|
|
647
|
+
content: `Failed to fetch providers: ${formatOpenCodeResponseError(providersResponse.error)}`,
|
|
644
648
|
components: [],
|
|
645
649
|
})
|
|
646
650
|
return
|
package/src/discord-bot.ts
CHANGED
|
@@ -130,11 +130,13 @@ import * as errore from "errore";
|
|
|
130
130
|
import { createLogger, formatErrorWithStack, LogPrefix } from "./logger.js";
|
|
131
131
|
import { writeHeapSnapshot, startHeapMonitor } from "./heap-monitor.js";
|
|
132
132
|
import { startTaskRunner } from "./task-runner.js";
|
|
133
|
+
import { setGlobalDispatcher, Agent } from "undici";
|
|
133
134
|
// Increase connection pool to prevent deadlock when multiple sessions have open SSE streams.
|
|
134
135
|
// Each session's event.subscribe() holds a connection; without enough connections,
|
|
135
136
|
// regular HTTP requests (question.reply, session.prompt) get blocked → deadlock.
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
setGlobalDispatcher(
|
|
138
|
+
new Agent({ headersTimeout: 0, bodyTimeout: 0, connections: 500 }),
|
|
139
|
+
);
|
|
138
140
|
|
|
139
141
|
const discordLogger = createLogger(LogPrefix.DISCORD);
|
|
140
142
|
const voiceLogger = createLogger(LogPrefix.VOICE);
|
package/src/errors.ts
CHANGED
|
@@ -168,6 +168,37 @@ export class GitCommandError extends createTaggedError({
|
|
|
168
168
|
message: 'Git command failed: $command',
|
|
169
169
|
}) {}
|
|
170
170
|
|
|
171
|
+
export function formatOpenCodeResponseError(error: unknown): string {
|
|
172
|
+
if (error && typeof error === 'object') {
|
|
173
|
+
if (
|
|
174
|
+
'data' in error &&
|
|
175
|
+
error.data &&
|
|
176
|
+
typeof error.data === 'object' &&
|
|
177
|
+
'message' in error.data
|
|
178
|
+
) {
|
|
179
|
+
return String(error.data.message)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
'errors' in error &&
|
|
184
|
+
Array.isArray(error.errors) &&
|
|
185
|
+
error.errors.length > 0
|
|
186
|
+
) {
|
|
187
|
+
return JSON.stringify(error.errors)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if ('message' in error && typeof error.message === 'string') {
|
|
191
|
+
return error.message
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if ('name' in error && typeof error.name === 'string') {
|
|
195
|
+
return error.name
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return 'Unknown OpenCode API error'
|
|
200
|
+
}
|
|
201
|
+
|
|
171
202
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
172
203
|
// UNION TYPES - For function signatures
|
|
173
204
|
// ═══════════════════════════════════════════════════════════════════════════
|
package/src/opencode.ts
CHANGED
|
@@ -82,8 +82,10 @@ const ANSI_ESCAPE_REGEX =
|
|
|
82
82
|
|
|
83
83
|
async function requestHealthcheck({
|
|
84
84
|
url,
|
|
85
|
+
timeoutMs = 2000,
|
|
85
86
|
}: {
|
|
86
87
|
url: string
|
|
88
|
+
timeoutMs?: number
|
|
87
89
|
}): Promise<{ status: number; body: string }> {
|
|
88
90
|
return new Promise((resolve, reject) => {
|
|
89
91
|
const req = http.request(
|
|
@@ -107,6 +109,13 @@ async function requestHealthcheck({
|
|
|
107
109
|
})
|
|
108
110
|
},
|
|
109
111
|
)
|
|
112
|
+
// Without a timeout, a stalled connection (TCP accepted during opencode
|
|
113
|
+
// startup before the HTTP layer is ready) hangs forever, wedging
|
|
114
|
+
// waitForServer on its first iteration and blocking the entire single
|
|
115
|
+
// server startup promise. Abort and let the poll loop retry instead.
|
|
116
|
+
req.setTimeout(timeoutMs, () => {
|
|
117
|
+
req.destroy(new Error(`healthcheck timed out after ${timeoutMs}ms`))
|
|
118
|
+
})
|
|
110
119
|
req.on('error', reject)
|
|
111
120
|
req.end()
|
|
112
121
|
})
|