opencode-pilot 0.16.3 → 0.16.5
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/package.json +1 -1
- package/service/actions.js +45 -11
- package/service/poll-service.js +6 -2
- package/service/repo-config.js +6 -1
- package/test/unit/actions.test.js +42 -0
- package/test/unit/repo-config.test.js +40 -0
package/package.json
CHANGED
package/service/actions.js
CHANGED
|
@@ -470,6 +470,8 @@ function runSpawn(args, options = {}) {
|
|
|
470
470
|
export async function createSessionViaApi(serverUrl, directory, prompt, options = {}) {
|
|
471
471
|
const fetchFn = options.fetch || fetch;
|
|
472
472
|
|
|
473
|
+
let session = null;
|
|
474
|
+
|
|
473
475
|
try {
|
|
474
476
|
// Step 1: Create a new session with the directory parameter
|
|
475
477
|
const sessionUrl = new URL('/session', serverUrl);
|
|
@@ -486,7 +488,7 @@ export async function createSessionViaApi(serverUrl, directory, prompt, options
|
|
|
486
488
|
throw new Error(`Failed to create session: ${createResponse.status} ${errorText}`);
|
|
487
489
|
}
|
|
488
490
|
|
|
489
|
-
|
|
491
|
+
session = await createResponse.json();
|
|
490
492
|
debug(`createSessionViaApi: created session ${session.id} in ${directory}`);
|
|
491
493
|
|
|
492
494
|
// Step 2: Update session title if provided
|
|
@@ -523,19 +525,40 @@ export async function createSessionViaApi(serverUrl, directory, prompt, options
|
|
|
523
525
|
messageBody.modelID = modelID;
|
|
524
526
|
}
|
|
525
527
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
528
|
+
// Use AbortController with timeout for the message POST
|
|
529
|
+
// The /session/{id}/message endpoint returns a chunked/streaming response
|
|
530
|
+
// that stays open until the agent completes. We only need to verify the
|
|
531
|
+
// request was accepted (2xx status), not wait for the full response.
|
|
532
|
+
const controller = new AbortController();
|
|
533
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
|
531
534
|
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
+
try {
|
|
536
|
+
const messageResponse = await fetchFn(messageUrl.toString(), {
|
|
537
|
+
method: 'POST',
|
|
538
|
+
headers: { 'Content-Type': 'application/json' },
|
|
539
|
+
body: JSON.stringify(messageBody),
|
|
540
|
+
signal: controller.signal,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
clearTimeout(timeoutId);
|
|
544
|
+
|
|
545
|
+
if (!messageResponse.ok) {
|
|
546
|
+
const errorText = await messageResponse.text();
|
|
547
|
+
throw new Error(`Failed to send message: ${messageResponse.status} ${errorText}`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
debug(`createSessionViaApi: sent message to session ${session.id}`);
|
|
551
|
+
} catch (abortErr) {
|
|
552
|
+
clearTimeout(timeoutId);
|
|
553
|
+
// AbortError is expected - we intentionally abort after verifying the request started
|
|
554
|
+
// The server accepted our message, we just don't need to wait for the response
|
|
555
|
+
if (abortErr.name === 'AbortError') {
|
|
556
|
+
debug(`createSessionViaApi: message request started for session ${session.id} (response aborted as expected)`);
|
|
557
|
+
} else {
|
|
558
|
+
throw abortErr;
|
|
559
|
+
}
|
|
535
560
|
}
|
|
536
561
|
|
|
537
|
-
debug(`createSessionViaApi: sent message to session ${session.id}`);
|
|
538
|
-
|
|
539
562
|
return {
|
|
540
563
|
success: true,
|
|
541
564
|
sessionId: session.id,
|
|
@@ -543,6 +566,17 @@ export async function createSessionViaApi(serverUrl, directory, prompt, options
|
|
|
543
566
|
};
|
|
544
567
|
} catch (err) {
|
|
545
568
|
debug(`createSessionViaApi: error - ${err.message}`);
|
|
569
|
+
// If session was created but message failed, still return success
|
|
570
|
+
// to prevent re-processing (the session exists, user can send message manually)
|
|
571
|
+
if (session) {
|
|
572
|
+
debug(`createSessionViaApi: session ${session.id} was created, marking as success despite message error`);
|
|
573
|
+
return {
|
|
574
|
+
success: true,
|
|
575
|
+
sessionId: session.id,
|
|
576
|
+
directory,
|
|
577
|
+
warning: err.message,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
546
580
|
return {
|
|
547
581
|
success: false,
|
|
548
582
|
error: err.message,
|
package/service/poll-service.js
CHANGED
|
@@ -227,9 +227,13 @@ export async function pollOnce(options = {}) {
|
|
|
227
227
|
itemUpdatedAt: item.updated_at || null,
|
|
228
228
|
});
|
|
229
229
|
}
|
|
230
|
-
|
|
230
|
+
if (result.warning) {
|
|
231
|
+
console.log(`[poll] Started session for ${item.id} (warning: ${result.warning})`);
|
|
232
|
+
} else {
|
|
233
|
+
console.log(`[poll] Started session for ${item.id}`);
|
|
234
|
+
}
|
|
231
235
|
} else {
|
|
232
|
-
console.error(`[poll] Failed to start session: ${result.stderr}`);
|
|
236
|
+
console.error(`[poll] Failed to start session: ${result.error || result.stderr || 'unknown error'}`);
|
|
233
237
|
}
|
|
234
238
|
} catch (err) {
|
|
235
239
|
console.error(`[poll] Error executing action: ${err.message}`);
|
package/service/repo-config.js
CHANGED
|
@@ -352,7 +352,12 @@ export function resolveRepoForItem(source, item) {
|
|
|
352
352
|
if (resolvedRepo) {
|
|
353
353
|
return source.repos.includes(resolvedRepo) ? [resolvedRepo] : [];
|
|
354
354
|
}
|
|
355
|
-
// No repo template -
|
|
355
|
+
// No repo template - if exactly one repo, use it as default
|
|
356
|
+
// (e.g., Linear issues don't have repo context, user explicitly configures one repo)
|
|
357
|
+
if (source.repos.length === 1) {
|
|
358
|
+
return source.repos;
|
|
359
|
+
}
|
|
360
|
+
// Multiple repos but can't match without item context
|
|
356
361
|
return [];
|
|
357
362
|
}
|
|
358
363
|
|
|
@@ -930,5 +930,47 @@ describe('actions.js', () => {
|
|
|
930
930
|
assert.strictEqual(messageBody.providerID, 'anthropic', 'Should parse provider from model');
|
|
931
931
|
assert.strictEqual(messageBody.modelID, 'claude-sonnet-4-20250514', 'Should parse model ID');
|
|
932
932
|
});
|
|
933
|
+
|
|
934
|
+
test('returns success with warning when session created but message fails', async () => {
|
|
935
|
+
const { createSessionViaApi } = await import('../../service/actions.js');
|
|
936
|
+
|
|
937
|
+
const mockSessionId = 'ses_partial123';
|
|
938
|
+
|
|
939
|
+
const mockFetch = async (url, opts) => {
|
|
940
|
+
const urlObj = new URL(url);
|
|
941
|
+
|
|
942
|
+
// Session creation succeeds
|
|
943
|
+
if (urlObj.pathname === '/session' && opts?.method === 'POST') {
|
|
944
|
+
return {
|
|
945
|
+
ok: true,
|
|
946
|
+
json: async () => ({ id: mockSessionId }),
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Message send fails
|
|
951
|
+
if (urlObj.pathname.includes('/message') && opts?.method === 'POST') {
|
|
952
|
+
return {
|
|
953
|
+
ok: false,
|
|
954
|
+
status: 500,
|
|
955
|
+
text: async () => 'Message send failed',
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return { ok: true, json: async () => ({}) };
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
const result = await createSessionViaApi(
|
|
963
|
+
'http://localhost:4096',
|
|
964
|
+
'/path/to/project',
|
|
965
|
+
'Fix the bug',
|
|
966
|
+
{ fetch: mockFetch }
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
// Should return success because session was created
|
|
970
|
+
assert.ok(result.success, 'Should return success when session was created');
|
|
971
|
+
assert.strictEqual(result.sessionId, mockSessionId, 'Should return session ID');
|
|
972
|
+
assert.ok(result.warning, 'Should include warning about message failure');
|
|
973
|
+
assert.ok(result.warning.includes('Failed to send message'), 'Warning should mention message failure');
|
|
974
|
+
});
|
|
933
975
|
});
|
|
934
976
|
});
|
|
@@ -769,6 +769,46 @@ sources:
|
|
|
769
769
|
assert.deepStrictEqual(resolveRepoForItem(source, filteredItem), []);
|
|
770
770
|
});
|
|
771
771
|
|
|
772
|
+
test('single-repo allowlist uses repo as default when no template', async () => {
|
|
773
|
+
// Linear issues don't have repository context - when exactly one repo is configured,
|
|
774
|
+
// use it as the default for all items from that source
|
|
775
|
+
writeFileSync(configPath, `
|
|
776
|
+
sources:
|
|
777
|
+
- preset: linear/my-issues
|
|
778
|
+
repos:
|
|
779
|
+
- 0din-ai/odin
|
|
780
|
+
`);
|
|
781
|
+
|
|
782
|
+
const { loadRepoConfig, getSources, resolveRepoForItem } = await import('../../service/repo-config.js');
|
|
783
|
+
loadRepoConfig(configPath);
|
|
784
|
+
const source = getSources()[0];
|
|
785
|
+
|
|
786
|
+
// Linear items don't have repository field
|
|
787
|
+
const linearItem = { id: 'linear:abc123', title: 'Fix bug', state: { name: 'In Progress' } };
|
|
788
|
+
assert.deepStrictEqual(resolveRepoForItem(source, linearItem), ['0din-ai/odin'],
|
|
789
|
+
'single-repo allowlist should use repo as default');
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test('multi-repo allowlist returns empty when no template match', async () => {
|
|
793
|
+
// With multiple repos and no way to determine which one, return empty
|
|
794
|
+
writeFileSync(configPath, `
|
|
795
|
+
sources:
|
|
796
|
+
- preset: linear/my-issues
|
|
797
|
+
repos:
|
|
798
|
+
- org/repo-a
|
|
799
|
+
- org/repo-b
|
|
800
|
+
`);
|
|
801
|
+
|
|
802
|
+
const { loadRepoConfig, getSources, resolveRepoForItem } = await import('../../service/repo-config.js');
|
|
803
|
+
loadRepoConfig(configPath);
|
|
804
|
+
const source = getSources()[0];
|
|
805
|
+
|
|
806
|
+
// Can't determine which of the 2 repos to use
|
|
807
|
+
const linearItem = { id: 'linear:abc123', title: 'Fix bug' };
|
|
808
|
+
assert.deepStrictEqual(resolveRepoForItem(source, linearItem), [],
|
|
809
|
+
'multi-repo allowlist should return empty when no template');
|
|
810
|
+
});
|
|
811
|
+
|
|
772
812
|
test('github presets include semantic session names', async () => {
|
|
773
813
|
writeFileSync(configPath, `
|
|
774
814
|
sources:
|