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.
@@ -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.7.tar.gz"
5
- sha256 "3d97d488ca8695f9785171e767b8a7a3eb65d2a8c5aca9eab3862b70cf6fc144"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-pilot",
3
- "version": "0.24.8",
3
+ "version": "0.24.10",
4
4
  "type": "module",
5
5
  "main": "plugin/index.js",
6
6
  "description": "Automation daemon for OpenCode - polls for work and spawns sessions",
@@ -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: lookupDirectory,
647
+ directory,
657
648
  fetch: options.fetch
658
649
  });
659
650
 
660
651
  if (sessions.length === 0) {
661
- debug(`findReusableSession: no sessions found for ${lookupDirectory}`);
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 ${lookupDirectory}`);
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 ${lookupDirectory}`);
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 scoped to the project directory
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', projectDir);
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 title if provided
725
- if (options.title) {
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({ title: options.title }),
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 project directory (for correct projectID scoping)
568
- // The worktree path is used for messages/commands (file operations)
569
- assert.strictEqual(sessionDirectory, "/proj", "Session should be scoped to project directory");
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 project directory (for correct projectID scoping)
622
- assert.strictEqual(sessionDirectory, "/proj", "Session should be scoped to project directory");
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 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").
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 sessionQueryDirectory = null;
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": (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
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/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: {} }),
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
- // 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");
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 project directory (for correct projectID scoping)
985
- assert.strictEqual(sessionDirectory, '/data/proj',
986
- 'Session creation should use project directory, not worktree');
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('uses projectDirectory for session creation, working directory for messages', async () => {
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 project directory (for correct projectID scoping)
1079
- assert.ok(createUrl.includes('%2Fhome%2Fuser%2Fcode%2Fodin'),
1080
- 'Session creation should use projectDirectory');
1081
- assert.ok(!createUrl.includes('worktree'),
1082
- 'Session creation should NOT use the worktree path');
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
- // Message should use the working directory (for file operations in the worktree)
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