opencode-pilot 0.24.8 → 0.24.10
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/Formula/opencode-pilot.rb +2 -2
- package/package.json +1 -1
- package/service/actions.js +26 -26
- package/test/integration/session-reuse.test.js +30 -30
- package/test/unit/actions.test.js +21 -64
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
class OpencodePilot < Formula
|
|
2
2
|
desc "Automation daemon for OpenCode - polls GitHub/Linear issues and spawns sessions"
|
|
3
3
|
homepage "https://github.com/athal7/opencode-pilot"
|
|
4
|
-
url "https://github.com/athal7/opencode-pilot/archive/refs/tags/v0.24.
|
|
5
|
-
sha256 "
|
|
4
|
+
url "https://github.com/athal7/opencode-pilot/archive/refs/tags/v0.24.9.tar.gz"
|
|
5
|
+
sha256 "b477a5677fb80456db3f4f6464dd2071d0efb56bdd29b8a8fc01e5d56638f588"
|
|
6
6
|
license "MIT"
|
|
7
7
|
|
|
8
8
|
depends_on "node"
|
package/package.json
CHANGED
package/service/actions.js
CHANGED
|
@@ -639,26 +639,17 @@ export async function sendMessageToSession(serverUrl, sessionId, directory, prom
|
|
|
639
639
|
* @param {string} directory - Working directory to match
|
|
640
640
|
* @param {object} [options] - Options
|
|
641
641
|
* @param {function} [options.fetch] - Custom fetch function (for testing)
|
|
642
|
-
* @param {string} [options.projectDirectory] - Project directory for session lookup
|
|
643
|
-
* (used instead of directory when provided, so sessions created with the project
|
|
644
|
-
* directory are found instead of old worktree-scoped sessions)
|
|
645
642
|
* @returns {Promise<object|null>} Session to reuse, or null
|
|
646
643
|
*/
|
|
647
644
|
export async function findReusableSession(serverUrl, directory, options = {}) {
|
|
648
|
-
// Use project directory for session lookup when available.
|
|
649
|
-
// Sessions created with v0.24.7+ are scoped to the project directory,
|
|
650
|
-
// while messages use the worktree directory. Query the project directory
|
|
651
|
-
// to find correctly-scoped sessions.
|
|
652
|
-
const lookupDirectory = options.projectDirectory || directory;
|
|
653
|
-
|
|
654
645
|
// Get sessions for this directory
|
|
655
646
|
const sessions = await listSessions(serverUrl, {
|
|
656
|
-
directory
|
|
647
|
+
directory,
|
|
657
648
|
fetch: options.fetch
|
|
658
649
|
});
|
|
659
650
|
|
|
660
651
|
if (sessions.length === 0) {
|
|
661
|
-
debug(`findReusableSession: no sessions found for ${
|
|
652
|
+
debug(`findReusableSession: no sessions found for ${directory}`);
|
|
662
653
|
return null;
|
|
663
654
|
}
|
|
664
655
|
|
|
@@ -666,11 +657,11 @@ export async function findReusableSession(serverUrl, directory, options = {}) {
|
|
|
666
657
|
const activeSessions = sessions.filter(s => !isSessionArchived(s));
|
|
667
658
|
|
|
668
659
|
if (activeSessions.length === 0) {
|
|
669
|
-
debug(`findReusableSession: all ${sessions.length} sessions are archived for ${
|
|
660
|
+
debug(`findReusableSession: all ${sessions.length} sessions are archived for ${directory}`);
|
|
670
661
|
return null;
|
|
671
662
|
}
|
|
672
663
|
|
|
673
|
-
debug(`findReusableSession: found ${activeSessions.length} active sessions for ${
|
|
664
|
+
debug(`findReusableSession: found ${activeSessions.length} active sessions for ${directory}`);
|
|
674
665
|
|
|
675
666
|
// Get statuses to prefer idle sessions
|
|
676
667
|
const statuses = await getSessionStatuses(serverUrl, { fetch: options.fetch });
|
|
@@ -696,16 +687,17 @@ export async function findReusableSession(serverUrl, directory, options = {}) {
|
|
|
696
687
|
export async function createSessionViaApi(serverUrl, directory, prompt, options = {}) {
|
|
697
688
|
const fetchFn = options.fetch || fetch;
|
|
698
689
|
const headerTimeout = options.headerTimeout || HEADER_TIMEOUT_MS;
|
|
699
|
-
// Use project directory for session creation (determines projectID in OpenCode).
|
|
700
|
-
// The working directory (which may be a worktree) is used for messages/commands.
|
|
701
690
|
const projectDir = options.projectDirectory || directory;
|
|
702
691
|
|
|
703
692
|
let session = null;
|
|
704
693
|
|
|
705
694
|
try {
|
|
706
|
-
// Step 1: Create session
|
|
695
|
+
// Step 1: Create session with the working directory (may be a worktree).
|
|
696
|
+
// This sets the session's working directory so the agent operates in the
|
|
697
|
+
// correct location. For worktree sessions, the server may assign
|
|
698
|
+
// projectID 'global' since sandbox paths don't match project worktrees.
|
|
707
699
|
const sessionUrl = new URL('/session', serverUrl);
|
|
708
|
-
sessionUrl.searchParams.set('directory',
|
|
700
|
+
sessionUrl.searchParams.set('directory', directory);
|
|
709
701
|
|
|
710
702
|
const createResponse = await fetchFn(sessionUrl.toString(), {
|
|
711
703
|
method: 'POST',
|
|
@@ -721,14 +713,20 @@ export async function createSessionViaApi(serverUrl, directory, prompt, options
|
|
|
721
713
|
session = await createResponse.json();
|
|
722
714
|
debug(`createSessionViaApi: created session ${session.id} in ${directory}`);
|
|
723
715
|
|
|
724
|
-
// Step 2: Update session
|
|
725
|
-
|
|
716
|
+
// Step 2: Update session - always PATCH with project directory when it
|
|
717
|
+
// differs from the working directory (worktree case). This re-associates
|
|
718
|
+
// the session with the correct project so it appears in the UI.
|
|
719
|
+
// Also set the title if provided.
|
|
720
|
+
const needsProjectScoping = projectDir !== directory;
|
|
721
|
+
if (options.title || needsProjectScoping) {
|
|
726
722
|
const updateUrl = new URL(`/session/${session.id}`, serverUrl);
|
|
727
723
|
updateUrl.searchParams.set('directory', projectDir);
|
|
724
|
+
const patchBody = {};
|
|
725
|
+
if (options.title) patchBody.title = options.title;
|
|
728
726
|
await fetchFn(updateUrl.toString(), {
|
|
729
727
|
method: 'PATCH',
|
|
730
728
|
headers: { 'Content-Type': 'application/json' },
|
|
731
|
-
body: JSON.stringify(
|
|
729
|
+
body: JSON.stringify(patchBody),
|
|
732
730
|
});
|
|
733
731
|
}
|
|
734
732
|
|
|
@@ -891,14 +889,16 @@ async function executeInDirectory(serverUrl, cwd, item, config, options = {}, pr
|
|
|
891
889
|
}
|
|
892
890
|
}
|
|
893
891
|
|
|
894
|
-
// Check if we should try to reuse an existing session
|
|
892
|
+
// Check if we should try to reuse an existing session.
|
|
893
|
+
// Skip reuse when working in a worktree (projectDirectory differs from cwd),
|
|
894
|
+
// because querying by worktree dir finds old sessions with projectID "global",
|
|
895
|
+
// and querying by project dir finds unrelated sessions for other PRs.
|
|
896
|
+
// Each worktree should get its own correctly-scoped session.
|
|
895
897
|
const reuseActiveSession = config.reuse_active_session !== false; // default true
|
|
898
|
+
const inWorktree = projectDirectory && projectDirectory !== cwd;
|
|
896
899
|
|
|
897
|
-
if (reuseActiveSession && !options.dryRun) {
|
|
898
|
-
const existingSession = await findReusableSession(serverUrl, cwd, {
|
|
899
|
-
fetch: options.fetch,
|
|
900
|
-
projectDirectory: projectDirectory,
|
|
901
|
-
});
|
|
900
|
+
if (reuseActiveSession && !inWorktree && !options.dryRun) {
|
|
901
|
+
const existingSession = await findReusableSession(serverUrl, cwd, { fetch: options.fetch });
|
|
902
902
|
|
|
903
903
|
if (existingSession) {
|
|
904
904
|
debug(`executeInDirectory: found reusable session ${existingSession.id} for ${cwd}`);
|
|
@@ -564,9 +564,9 @@ describe("integration: worktree creation with worktree_name", () => {
|
|
|
564
564
|
assert.ok(worktreeCreateCalled, "Should create worktree when worktree_name is configured");
|
|
565
565
|
assert.strictEqual(createdWorktreeName, "pr-42", "Should expand worktree_name template");
|
|
566
566
|
assert.ok(sessionCreated, "Should create session");
|
|
567
|
-
// Session creation uses the
|
|
568
|
-
// The
|
|
569
|
-
assert.strictEqual(sessionDirectory, "/
|
|
567
|
+
// Session creation uses the worktree directory (sets working directory)
|
|
568
|
+
// The PATCH with project directory re-associates it with the correct project
|
|
569
|
+
assert.strictEqual(sessionDirectory, "/worktree/pr-42", "Session should be created in worktree directory");
|
|
570
570
|
});
|
|
571
571
|
|
|
572
572
|
it("reuses stored directory when reprocessing same item", async () => {
|
|
@@ -618,18 +618,20 @@ describe("integration: worktree creation with worktree_name", () => {
|
|
|
618
618
|
assert.ok(result.success, "Action should succeed");
|
|
619
619
|
// Should NOT create a new worktree since we have existing_directory
|
|
620
620
|
assert.strictEqual(worktreeCreateCalled, false, "Should NOT create new worktree when existing_directory provided");
|
|
621
|
-
// Session creation uses the
|
|
622
|
-
assert.strictEqual(sessionDirectory,
|
|
621
|
+
// Session creation uses the existing worktree directory (sets working directory)
|
|
622
|
+
assert.strictEqual(sessionDirectory, existingWorktreeDir, "Session should be created in existing worktree directory");
|
|
623
623
|
});
|
|
624
624
|
|
|
625
|
-
it("session reuse
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
625
|
+
it("skips session reuse when working in a worktree", async () => {
|
|
626
|
+
// When working in a worktree (projectDirectory differs from cwd), session
|
|
627
|
+
// reuse is skipped entirely. Querying by worktree dir finds old sessions
|
|
628
|
+
// with projectID "global", querying by project dir finds unrelated sessions
|
|
629
|
+
// for other PRs. Each worktree should get its own correctly-scoped session.
|
|
630
630
|
|
|
631
|
-
let
|
|
631
|
+
let sessionListQueried = false;
|
|
632
632
|
let sessionCreated = false;
|
|
633
|
+
let sessionCreateDirectory = null;
|
|
634
|
+
let patchDirectory = null;
|
|
633
635
|
|
|
634
636
|
const existingWorktreeDir = "/worktree/calm-wizard";
|
|
635
637
|
|
|
@@ -643,26 +645,20 @@ describe("integration: worktree creation with worktree_name", () => {
|
|
|
643
645
|
"GET /experimental/worktree": () => ({
|
|
644
646
|
body: [existingWorktreeDir],
|
|
645
647
|
}),
|
|
646
|
-
"GET /session": (
|
|
647
|
-
|
|
648
|
-
// Return a session ONLY if queried by project directory
|
|
649
|
-
if (req.directory === "/proj") {
|
|
650
|
-
return {
|
|
651
|
-
body: [{ id: "ses_proj_scoped", directory: "/proj", time: { created: 1000, updated: 2000 } }],
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
// Old worktree-scoped sessions should NOT be found when querying by project dir
|
|
648
|
+
"GET /session": () => {
|
|
649
|
+
sessionListQueried = true;
|
|
655
650
|
return { body: [] };
|
|
656
651
|
},
|
|
657
652
|
"GET /session/status": () => ({ body: {} }),
|
|
658
653
|
"POST /session": (req) => {
|
|
659
654
|
sessionCreated = true;
|
|
655
|
+
sessionCreateDirectory = req.query?.directory;
|
|
660
656
|
return { body: { id: "ses_new" } };
|
|
661
657
|
},
|
|
662
|
-
"PATCH /session/
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
658
|
+
"PATCH /session/ses_new": (req) => {
|
|
659
|
+
patchDirectory = req.query?.directory;
|
|
660
|
+
return { body: {} };
|
|
661
|
+
},
|
|
666
662
|
"POST /session/ses_new/message": () => ({ body: { success: true } }),
|
|
667
663
|
"POST /session/ses_new/command": () => ({ body: { success: true } }),
|
|
668
664
|
});
|
|
@@ -679,12 +675,16 @@ describe("integration: worktree creation with worktree_name", () => {
|
|
|
679
675
|
);
|
|
680
676
|
|
|
681
677
|
assert.ok(result.success, "Action should succeed");
|
|
682
|
-
//
|
|
683
|
-
assert.strictEqual(
|
|
684
|
-
"
|
|
685
|
-
// Should
|
|
686
|
-
assert.
|
|
687
|
-
assert.strictEqual(
|
|
678
|
+
// Should NOT query for existing sessions when in a worktree
|
|
679
|
+
assert.strictEqual(sessionListQueried, false,
|
|
680
|
+
"Should skip session reuse entirely when in a worktree");
|
|
681
|
+
// Should create a new session with the worktree directory (correct working dir)
|
|
682
|
+
assert.ok(sessionCreated, "Should create a new session");
|
|
683
|
+
assert.strictEqual(sessionCreateDirectory, existingWorktreeDir,
|
|
684
|
+
"Session should be created in worktree directory");
|
|
685
|
+
// PATCH re-associates the session with the correct project
|
|
686
|
+
assert.strictEqual(patchDirectory, "/proj",
|
|
687
|
+
"PATCH should use project directory for correct project scoping");
|
|
688
688
|
});
|
|
689
689
|
});
|
|
690
690
|
|
|
@@ -981,9 +981,9 @@ Check for bugs and security issues.`;
|
|
|
981
981
|
// Should NOT call worktree endpoints when existing_directory is provided
|
|
982
982
|
assert.strictEqual(worktreeListCalled, false, 'Should NOT list worktrees');
|
|
983
983
|
assert.strictEqual(worktreeCreateCalled, false, 'Should NOT create worktree');
|
|
984
|
-
// Session creation uses
|
|
985
|
-
assert.strictEqual(sessionDirectory, '/data/
|
|
986
|
-
'Session creation should use
|
|
984
|
+
// Session creation uses the worktree directory (sets correct working directory)
|
|
985
|
+
assert.strictEqual(sessionDirectory, '/data/worktree/calm-wizard',
|
|
986
|
+
'Session creation should use worktree directory for correct working dir');
|
|
987
987
|
// Result directory is the worktree (where file operations happen)
|
|
988
988
|
assert.strictEqual(result.directory, '/data/worktree/calm-wizard',
|
|
989
989
|
'Result should include worktree directory');
|
|
@@ -1042,11 +1042,12 @@ Check for bugs and security issues.`;
|
|
|
1042
1042
|
assert.ok(messageUrl.includes('%2Fpath%2Fto%2Fproject'), 'Message URL should include encoded directory path');
|
|
1043
1043
|
});
|
|
1044
1044
|
|
|
1045
|
-
test('
|
|
1045
|
+
test('creates session with worktree directory, patches with project directory for scoping', async () => {
|
|
1046
1046
|
const { createSessionViaApi } = await import('../../service/actions.js');
|
|
1047
1047
|
|
|
1048
1048
|
const mockSessionId = 'ses_test_proj';
|
|
1049
1049
|
let createUrl = null;
|
|
1050
|
+
let patchUrl = null;
|
|
1050
1051
|
let messageUrl = null;
|
|
1051
1052
|
|
|
1052
1053
|
const mockFetch = async (url, opts) => {
|
|
@@ -1057,6 +1058,11 @@ Check for bugs and security issues.`;
|
|
|
1057
1058
|
return { ok: true, json: async () => ({ id: mockSessionId }) };
|
|
1058
1059
|
}
|
|
1059
1060
|
|
|
1061
|
+
if (urlObj.pathname.includes(`/session/${mockSessionId}`) && opts?.method === 'PATCH') {
|
|
1062
|
+
patchUrl = url;
|
|
1063
|
+
return { ok: true, json: async () => ({}) };
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1060
1066
|
if (urlObj.pathname.includes('/message') && opts?.method === 'POST') {
|
|
1061
1067
|
messageUrl = url;
|
|
1062
1068
|
return { ok: true, json: async () => ({ success: true }) };
|
|
@@ -1075,13 +1081,18 @@ Check for bugs and security issues.`;
|
|
|
1075
1081
|
}
|
|
1076
1082
|
);
|
|
1077
1083
|
|
|
1078
|
-
// Session creation should use the
|
|
1079
|
-
assert.ok(createUrl.includes('
|
|
1080
|
-
'Session creation should use
|
|
1081
|
-
assert.ok(
|
|
1082
|
-
'Session creation should
|
|
1084
|
+
// Session creation should use the worktree directory (sets working directory)
|
|
1085
|
+
assert.ok(createUrl.includes('worktree'),
|
|
1086
|
+
'Session creation should use the worktree path');
|
|
1087
|
+
assert.ok(createUrl.includes('pr-415'),
|
|
1088
|
+
'Session creation should use the worktree path');
|
|
1083
1089
|
|
|
1084
|
-
//
|
|
1090
|
+
// PATCH should use the project directory (re-associates with correct project)
|
|
1091
|
+
assert.ok(patchUrl, 'PATCH should be called for project scoping');
|
|
1092
|
+
assert.ok(patchUrl.includes('%2Fhome%2Fuser%2Fcode%2Fodin'),
|
|
1093
|
+
'PATCH should use projectDirectory for project scoping');
|
|
1094
|
+
|
|
1095
|
+
// Message should use the working directory (worktree)
|
|
1085
1096
|
assert.ok(messageUrl.includes('worktree'),
|
|
1086
1097
|
'Message should use the worktree working directory');
|
|
1087
1098
|
assert.ok(messageUrl.includes('pr-415'),
|
|
@@ -2147,60 +2158,6 @@ Check for bugs and security issues.`;
|
|
|
2147
2158
|
assert.strictEqual(result.id, 'ses_new');
|
|
2148
2159
|
});
|
|
2149
2160
|
|
|
2150
|
-
test('uses projectDirectory for session lookup when provided', async () => {
|
|
2151
|
-
const { findReusableSession } = await import('../../service/actions.js');
|
|
2152
|
-
|
|
2153
|
-
let queriedDirectory = null;
|
|
2154
|
-
const mockFetch = async (url) => {
|
|
2155
|
-
if (url.includes('/session/status')) {
|
|
2156
|
-
return { ok: true, json: async () => ({}) };
|
|
2157
|
-
}
|
|
2158
|
-
// Capture the directory used in the session query
|
|
2159
|
-
const urlObj = new URL(url);
|
|
2160
|
-
queriedDirectory = urlObj.searchParams.get('directory');
|
|
2161
|
-
return {
|
|
2162
|
-
ok: true,
|
|
2163
|
-
json: async () => [
|
|
2164
|
-
{ id: 'ses_proj', time: { created: 1000, updated: 2000 } },
|
|
2165
|
-
],
|
|
2166
|
-
};
|
|
2167
|
-
};
|
|
2168
|
-
|
|
2169
|
-
const result = await findReusableSession('http://localhost:4096', '/worktree/pr-415', {
|
|
2170
|
-
fetch: mockFetch,
|
|
2171
|
-
projectDirectory: '/home/user/code/odin',
|
|
2172
|
-
});
|
|
2173
|
-
|
|
2174
|
-
assert.strictEqual(result.id, 'ses_proj');
|
|
2175
|
-
assert.strictEqual(queriedDirectory, '/home/user/code/odin',
|
|
2176
|
-
'Should query sessions using projectDirectory, not the worktree directory');
|
|
2177
|
-
});
|
|
2178
|
-
|
|
2179
|
-
test('falls back to directory when projectDirectory not provided', async () => {
|
|
2180
|
-
const { findReusableSession } = await import('../../service/actions.js');
|
|
2181
|
-
|
|
2182
|
-
let queriedDirectory = null;
|
|
2183
|
-
const mockFetch = async (url) => {
|
|
2184
|
-
if (url.includes('/session/status')) {
|
|
2185
|
-
return { ok: true, json: async () => ({}) };
|
|
2186
|
-
}
|
|
2187
|
-
const urlObj = new URL(url);
|
|
2188
|
-
queriedDirectory = urlObj.searchParams.get('directory');
|
|
2189
|
-
return {
|
|
2190
|
-
ok: true,
|
|
2191
|
-
json: async () => [
|
|
2192
|
-
{ id: 'ses_1', time: { created: 1000, updated: 2000 } },
|
|
2193
|
-
],
|
|
2194
|
-
};
|
|
2195
|
-
};
|
|
2196
|
-
|
|
2197
|
-
const result = await findReusableSession('http://localhost:4096', '/test/dir', { fetch: mockFetch });
|
|
2198
|
-
|
|
2199
|
-
assert.strictEqual(result.id, 'ses_1');
|
|
2200
|
-
assert.strictEqual(queriedDirectory, '/test/dir',
|
|
2201
|
-
'Should query sessions using directory when projectDirectory not provided');
|
|
2202
|
-
});
|
|
2203
|
-
|
|
2204
2161
|
test('falls back to busy session when no idle available', async () => {
|
|
2205
2162
|
const { findReusableSession } = await import('../../service/actions.js');
|
|
2206
2163
|
|