1+ import { DEFAULT_FORGE_CONFIG } from "./forge-config.js" ;
12import { normalizeLocalStoreDbPath , openLocalStoreDb , resolveLocalStoreDbPath } from "./local-store.js" ;
23import { applySchemaMigrations } from "./schema-version.js" ;
34
@@ -28,9 +29,43 @@ function normalizeRunState(state) {
2829 throw new Error ( "invalid_run_state" ) ;
2930}
3031
32+ /** Optional forge host, scoping rows so two hosts serving the same owner/repo name never collide (#5563).
33+ * Omitted/nullish → the github.com default, so every pre-existing single-forge caller is unaffected. */
34+ function normalizeApiBaseUrl ( apiBaseUrl ) {
35+ if ( apiBaseUrl === undefined || apiBaseUrl === null ) return DEFAULT_FORGE_CONFIG . apiBaseUrl ;
36+ if ( typeof apiBaseUrl !== "string" || ! apiBaseUrl . trim ( ) ) throw new Error ( "invalid_api_base_url" ) ;
37+ return apiBaseUrl . trim ( ) ;
38+ }
39+
40+ // v1 -> v2 (#5563): rebuild the bare `repo_full_name` PRIMARY KEY into a (api_base_url, repo_full_name) composite
41+ // -- two forge hosts serving a same-named owner/repo must not share one "current state" row. SQLite cannot ALTER
42+ // a PRIMARY KEY in place, so this rebuilds the table: create the new shape, copy every existing row with the
43+ // pre-#4784 implicit single-forge default backfilled, drop the old table, rename the new one in.
44+ function addApiBaseUrlScope ( db ) {
45+ db . exec ( `
46+ CREATE TABLE miner_run_state_v2 (
47+ api_base_url TEXT NOT NULL,
48+ repo_full_name TEXT NOT NULL,
49+ state TEXT NOT NULL CHECK (state IN ('idle', 'discovering', 'planning', 'preparing')),
50+ updated_at TEXT NOT NULL,
51+ PRIMARY KEY (api_base_url, repo_full_name)
52+ )
53+ ` ) ;
54+ // OR IGNORE: a row this store's own read path already treats as unusable garbage (an unrecognized `state`,
55+ // e.g. from a hand-edited or otherwise corrupted file -- getRunState/listRunStates fail closed on it too)
56+ // would violate the CHECK constraint above and abort the whole migration. Skipping it here is consistent with
57+ // that same fail-closed posture, rather than turning one bad row into a permanently unmigratable file.
58+ db . prepare (
59+ `INSERT OR IGNORE INTO miner_run_state_v2 (api_base_url, repo_full_name, state, updated_at)
60+ SELECT ?, repo_full_name, state, updated_at FROM miner_run_state` ,
61+ ) . run ( DEFAULT_FORGE_CONFIG . apiBaseUrl ) ;
62+ db . exec ( "DROP TABLE miner_run_state" ) ;
63+ db . exec ( "ALTER TABLE miner_run_state_v2 RENAME TO miner_run_state" ) ;
64+ }
65+
3166/**
3267 * Opens the 100% local/client-side miner run-state store. The database only lives on this machine;
33- * this module never uploads, syncs, or phones home with its contents. (#2289)
68+ * this module never uploads, syncs, or phones home with its contents. (#2289, #5563 )
3469 */
3570export function initRunStateStore ( dbPath = resolveRunStateDbPath ( ) ) {
3671 const resolvedPath = normalizeDbPath ( dbPath ) ;
@@ -42,42 +77,48 @@ export function initRunStateStore(dbPath = resolveRunStateDbPath()) {
4277 updated_at TEXT NOT NULL
4378 )
4479 ` ) ;
45- // Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations (none yet) .
46- applySchemaMigrations ( db , [ ] ) ;
80+ // Schema-version convention (#4832): stamp the baseline and run any post-baseline migrations.
81+ applySchemaMigrations ( db , [ addApiBaseUrlScope ] ) ;
4782
4883 const getStatement = db . prepare (
49- "SELECT state FROM miner_run_state WHERE repo_full_name = ?" ,
84+ "SELECT state FROM miner_run_state WHERE api_base_url = ? AND repo_full_name = ?" ,
5085 ) ;
5186 const setStatement = db . prepare ( `
52- INSERT INTO miner_run_state (repo_full_name, state, updated_at)
53- VALUES (?, ?, ?)
54- ON CONFLICT(repo_full_name) DO UPDATE SET
87+ INSERT INTO miner_run_state (api_base_url, repo_full_name, state, updated_at)
88+ VALUES (?, ?, ?, ? )
89+ ON CONFLICT(api_base_url, repo_full_name) DO UPDATE SET
5590 state = excluded.state,
5691 updated_at = excluded.updated_at
5792 ` ) ;
5893 const listStatement = db . prepare (
59- "SELECT repo_full_name, state, updated_at FROM miner_run_state ORDER BY repo_full_name" ,
94+ "SELECT api_base_url, repo_full_name, state, updated_at FROM miner_run_state ORDER BY repo_full_name" ,
6095 ) ;
6196
6297 return {
6398 dbPath : resolvedPath ,
64- getRunState ( repoFullName ) {
65- const row = getStatement . get ( normalizeRepoFullName ( repoFullName ) ) ;
99+ getRunState ( repoFullName , apiBaseUrl ) {
100+ const row = getStatement . get ( normalizeApiBaseUrl ( apiBaseUrl ) , normalizeRepoFullName ( repoFullName ) ) ;
66101 return runStateSet . has ( row ?. state ) ? row . state : null ;
67102 } ,
68- setRunState ( repoFullName , state ) {
103+ setRunState ( repoFullName , state , apiBaseUrl ) {
104+ const normalizedForge = normalizeApiBaseUrl ( apiBaseUrl ) ;
69105 const normalizedRepo = normalizeRepoFullName ( repoFullName ) ;
70106 const normalizedState = normalizeRunState ( state ) ;
71107 const updatedAt = new Date ( ) . toISOString ( ) ;
72- setStatement . run ( normalizedRepo , normalizedState , updatedAt ) ;
73- return { repoFullName : normalizedRepo , state : normalizedState , updatedAt } ;
108+ setStatement . run ( normalizedForge , normalizedRepo , normalizedState , updatedAt ) ;
109+ return { apiBaseUrl : normalizedForge , repoFullName : normalizedRepo , state : normalizedState , updatedAt } ;
74110 } ,
75111 /** Every repo with a recorded run state, across the whole store — the per-repo discover/plan/prepare
76112 * signal a "run portfolio" view folds alongside managed PR rows (#4279). */
77113 listRunStates ( ) {
78114 return listStatement . all ( )
79115 . filter ( ( row ) => runStateSet . has ( row . state ) )
80- . map ( ( row ) => ( { repoFullName : row . repo_full_name , state : row . state , updatedAt : row . updated_at } ) ) ;
116+ . map ( ( row ) => ( {
117+ apiBaseUrl : row . api_base_url ,
118+ repoFullName : row . repo_full_name ,
119+ state : row . state ,
120+ updatedAt : row . updated_at ,
121+ } ) ) ;
81122 } ,
82123 close ( ) {
83124 db . close ( ) ;
@@ -90,12 +131,12 @@ function getDefaultRunStateStore() {
90131 return defaultRunStateStore ;
91132}
92133
93- export function getRunState ( repoFullName ) {
94- return getDefaultRunStateStore ( ) . getRunState ( repoFullName ) ;
134+ export function getRunState ( repoFullName , apiBaseUrl ) {
135+ return getDefaultRunStateStore ( ) . getRunState ( repoFullName , apiBaseUrl ) ;
95136}
96137
97- export function setRunState ( repoFullName , state ) {
98- return getDefaultRunStateStore ( ) . setRunState ( repoFullName , state ) ;
138+ export function setRunState ( repoFullName , state , apiBaseUrl ) {
139+ return getDefaultRunStateStore ( ) . setRunState ( repoFullName , state , apiBaseUrl ) ;
99140}
100141
101142export function listRunStates ( ) {
0 commit comments