jettypod 4.4.56 → 4.4.57

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/lib/merge-lock.js CHANGED
@@ -43,6 +43,8 @@ async function acquireMergeLock(db, workItemId, instanceId = null, options = {})
43
43
  const lockedBy = instanceId || `${os.hostname()}-${process.pid}`;
44
44
 
45
45
  let pollCount = 0;
46
+ let lastStatusTime = 0;
47
+ const statusInterval = 10000; // Show status every 10 seconds
46
48
 
47
49
  while (Date.now() - startTime < maxWait) {
48
50
  // Cleanup stale locks every 5 polls (every ~10 seconds with 2s poll interval)
@@ -74,6 +76,21 @@ async function acquireMergeLock(db, workItemId, instanceId = null, options = {})
74
76
  // Race condition - someone else got it first
75
77
  // Continue polling
76
78
  }
79
+ } else {
80
+ // Show progress when waiting for existing lock
81
+ const now = Date.now();
82
+ if (now - lastStatusTime >= statusInterval) {
83
+ const waitedSeconds = Math.round((now - startTime) / 1000);
84
+ const lockAge = await getLockAge(db, existingLock.id);
85
+ const staleIn = Math.max(0, Math.round((staleThreshold - lockAge) / 1000));
86
+
87
+ if (staleIn > 0) {
88
+ console.log(`⏳ Waiting for merge lock... (${waitedSeconds}s elapsed, lock expires in ~${staleIn}s)`);
89
+ } else {
90
+ console.log(`⏳ Waiting for merge lock... (${waitedSeconds}s elapsed, cleaning up stale lock)`);
91
+ }
92
+ lastStatusTime = now;
93
+ }
77
94
  }
78
95
 
79
96
  // Wait before retrying
@@ -98,6 +115,27 @@ function checkExistingLock(db) {
98
115
  });
99
116
  }
100
117
 
118
+ /**
119
+ * Get the age of a lock in milliseconds
120
+ *
121
+ * @param {Object} db - SQLite database connection
122
+ * @param {number} lockId - Lock ID
123
+ * @returns {Promise<number>} Age in milliseconds
124
+ */
125
+ function getLockAge(db, lockId) {
126
+ return new Promise((resolve, reject) => {
127
+ db.get(
128
+ `SELECT (julianday('now') - julianday(locked_at)) * 86400000 as age_ms
129
+ FROM merge_locks WHERE id = ?`,
130
+ [lockId],
131
+ (err, row) => {
132
+ if (err) reject(err);
133
+ else resolve(row ? row.age_ms : 0);
134
+ }
135
+ );
136
+ });
137
+ }
138
+
101
139
  /**
102
140
  * Insert lock record into database
103
141
  *
@@ -1,7 +1,7 @@
1
1
  const { execSync } = require('child_process');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
- const { getDb, getDbPath, getJettypodDir } = require('../../lib/database');
4
+ const { getDb, getDbPath, getJettypodDir, waitForMigrations } = require('../../lib/database');
5
5
  const { getCurrentWork, setCurrentWork, clearCurrentWork, getCurrentWorkPath } = require('../../lib/current-work');
6
6
  const { VALID_STATUSES } = require('../../lib/constants');
7
7
  const { updateCurrentWork } = require('../../lib/claudemd');
@@ -1230,6 +1230,9 @@ async function mergeWork(options = {}) {
1230
1230
  const mergeLock = require('../../lib/merge-lock');
1231
1231
  const db = getDb();
1232
1232
 
1233
+ // Wait for database migrations to complete before using merge_locks table
1234
+ await waitForMigrations();
1235
+
1233
1236
  console.log('⏳ Acquiring merge lock...');
1234
1237
  let lock;
1235
1238
  try {
@@ -1249,6 +1252,24 @@ async function mergeWork(options = {}) {
1249
1252
  return Promise.reject(new Error(`Failed to acquire merge lock: ${lockErr.message}`));
1250
1253
  }
1251
1254
 
1255
+ // Set up signal handlers to release lock on interrupt (Ctrl+C, kill, etc.)
1256
+ // Without this, process termination leaves orphan locks that block for 90 seconds
1257
+ const cleanupAndExit = async (signal) => {
1258
+ console.log(`\n⚠️ Received ${signal}, releasing merge lock...`);
1259
+ if (lock) {
1260
+ try {
1261
+ await lock.release();
1262
+ console.log('✅ Merge lock released');
1263
+ } catch (err) {
1264
+ console.warn(`Warning: Failed to release lock: ${err.message}`);
1265
+ }
1266
+ }
1267
+ process.exit(1);
1268
+ };
1269
+
1270
+ process.on('SIGINT', cleanupAndExit);
1271
+ process.on('SIGTERM', cleanupAndExit);
1272
+
1252
1273
  // Wrap all merge operations in try/finally to ensure lock release
1253
1274
  try {
1254
1275
  // Get feature branch - either passed explicitly, from worktree DB, or from current CWD
@@ -1617,6 +1638,10 @@ async function mergeWork(options = {}) {
1617
1638
 
1618
1639
  return Promise.resolve();
1619
1640
  } finally {
1641
+ // Remove signal handlers to avoid interfering with other operations
1642
+ process.removeListener('SIGINT', cleanupAndExit);
1643
+ process.removeListener('SIGTERM', cleanupAndExit);
1644
+
1620
1645
  // Release lock unless holding for transition
1621
1646
  if (lock && !withTransition) {
1622
1647
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jettypod",
3
- "version": "4.4.56",
3
+ "version": "4.4.57",
4
4
  "description": "AI-powered development workflow manager with TDD, BDD, and automatic test generation",
5
5
  "main": "jettypod.js",
6
6
  "bin": {