opencode-pilot 0.24.7 → 0.24.8

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.
@@ -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.6.tar.gz"
5
- sha256 "60f487bfbe6209da3687d854b848b920791dde739046fd72bf75594d6e5e6163"
4
+ url "https://github.com/athal7/opencode-pilot/archive/refs/tags/v0.24.7.tar.gz"
5
+ sha256 "3d97d488ca8695f9785171e767b8a7a3eb65d2a8c5aca9eab3862b70cf6fc144"
6
6
  license "MIT"
7
7
 
8
8
  depends_on "node"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-pilot",
3
- "version": "0.24.7",
3
+ "version": "0.24.8",
4
4
  "type": "module",
5
5
  "main": "plugin/index.js",
6
6
  "description": "Automation daemon for OpenCode - polls for work and spawns sessions",
@@ -639,17 +639,26 @@ 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)
642
645
  * @returns {Promise<object|null>} Session to reuse, or null
643
646
  */
644
647
  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
+
645
654
  // Get sessions for this directory
646
655
  const sessions = await listSessions(serverUrl, {
647
- directory,
656
+ directory: lookupDirectory,
648
657
  fetch: options.fetch
649
658
  });
650
659
 
651
660
  if (sessions.length === 0) {
652
- debug(`findReusableSession: no sessions found for ${directory}`);
661
+ debug(`findReusableSession: no sessions found for ${lookupDirectory}`);
653
662
  return null;
654
663
  }
655
664
 
@@ -657,11 +666,11 @@ export async function findReusableSession(serverUrl, directory, options = {}) {
657
666
  const activeSessions = sessions.filter(s => !isSessionArchived(s));
658
667
 
659
668
  if (activeSessions.length === 0) {
660
- debug(`findReusableSession: all ${sessions.length} sessions are archived for ${directory}`);
669
+ debug(`findReusableSession: all ${sessions.length} sessions are archived for ${lookupDirectory}`);
661
670
  return null;
662
671
  }
663
672
 
664
- debug(`findReusableSession: found ${activeSessions.length} active sessions for ${directory}`);
673
+ debug(`findReusableSession: found ${activeSessions.length} active sessions for ${lookupDirectory}`);
665
674
 
666
675
  // Get statuses to prefer idle sessions
667
676
  const statuses = await getSessionStatuses(serverUrl, { fetch: options.fetch });
@@ -886,7 +895,10 @@ async function executeInDirectory(serverUrl, cwd, item, config, options = {}, pr
886
895
  const reuseActiveSession = config.reuse_active_session !== false; // default true
887
896
 
888
897
  if (reuseActiveSession && !options.dryRun) {
889
- const existingSession = await findReusableSession(serverUrl, cwd, { fetch: options.fetch });
898
+ const existingSession = await findReusableSession(serverUrl, cwd, {
899
+ fetch: options.fetch,
900
+ projectDirectory: projectDirectory,
901
+ });
890
902
 
891
903
  if (existingSession) {
892
904
  debug(`executeInDirectory: found reusable session ${existingSession.id} for ${cwd}`);
@@ -621,6 +621,71 @@ describe("integration: worktree creation with worktree_name", () => {
621
621
  // Session creation uses the project directory (for correct projectID scoping)
622
622
  assert.strictEqual(sessionDirectory, "/proj", "Session should be scoped to project directory");
623
623
  });
624
+
625
+ it("session reuse queries by project directory, not worktree directory", async () => {
626
+ // This tests the key fix: when reprocessing an item with a worktree,
627
+ // findReusableSession should query by the project directory (e.g., /proj)
628
+ // so it finds sessions created with correct scoping (v0.24.7+), rather
629
+ // than finding old sessions created with the worktree directory (projectID "global").
630
+
631
+ let sessionQueryDirectory = null;
632
+ let sessionCreated = false;
633
+
634
+ const existingWorktreeDir = "/worktree/calm-wizard";
635
+
636
+ mockServer = await createMockServer({
637
+ "GET /project": () => ({
638
+ body: [{ id: "proj_1", worktree: "/proj", sandboxes: [], time: { created: 1 } }],
639
+ }),
640
+ "GET /project/current": () => ({
641
+ body: { id: "proj_1", worktree: "/proj", sandboxes: [], time: { created: 1 } },
642
+ }),
643
+ "GET /experimental/worktree": () => ({
644
+ body: [existingWorktreeDir],
645
+ }),
646
+ "GET /session": (req) => {
647
+ sessionQueryDirectory = req.directory;
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
655
+ return { body: [] };
656
+ },
657
+ "GET /session/status": () => ({ body: {} }),
658
+ "POST /session": (req) => {
659
+ sessionCreated = true;
660
+ return { body: { id: "ses_new" } };
661
+ },
662
+ "PATCH /session/ses_proj_scoped": () => ({ body: {} }),
663
+ "POST /session/ses_proj_scoped/message": () => ({ body: { success: true } }),
664
+ "POST /session/ses_proj_scoped/command": () => ({ body: { success: true } }),
665
+ "PATCH /session/ses_new": () => ({ body: {} }),
666
+ "POST /session/ses_new/message": () => ({ body: { success: true } }),
667
+ "POST /session/ses_new/command": () => ({ body: { success: true } }),
668
+ });
669
+
670
+ const result = await executeAction(
671
+ { number: 42, title: "Review PR" },
672
+ {
673
+ path: "/proj",
674
+ prompt: "review",
675
+ worktree_name: "pr-{number}",
676
+ existing_directory: existingWorktreeDir,
677
+ },
678
+ { discoverServer: async () => mockServer.url }
679
+ );
680
+
681
+ assert.ok(result.success, "Action should succeed");
682
+ // The session lookup should use the PROJECT directory, not the worktree directory
683
+ assert.strictEqual(sessionQueryDirectory, "/proj",
684
+ "findReusableSession should query by project directory, not worktree");
685
+ // Should reuse the project-scoped session, NOT create a new one
686
+ assert.strictEqual(result.sessionReused, true, "Should reuse the project-scoped session");
687
+ assert.strictEqual(sessionCreated, false, "Should NOT create a new session when project-scoped session exists");
688
+ });
624
689
  });
625
690
 
626
691
  describe("integration: cross-source deduplication", () => {
@@ -2147,6 +2147,60 @@ Check for bugs and security issues.`;
2147
2147
  assert.strictEqual(result.id, 'ses_new');
2148
2148
  });
2149
2149
 
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
+
2150
2204
  test('falls back to busy session when no idle available', async () => {
2151
2205
  const { findReusableSession } = await import('../../service/actions.js');
2152
2206