@masslessai/push-todo 3.10.8 → 4.0.0
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/daemon-health.js +29 -1
- package/lib/daemon.js +82 -8
- package/package.json +1 -1
package/lib/daemon-health.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
11
|
-
import { spawn } from 'child_process';
|
|
11
|
+
import { spawn, execFileSync } from 'child_process';
|
|
12
12
|
import { homedir } from 'os';
|
|
13
13
|
import { join, dirname } from 'path';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
@@ -116,6 +116,30 @@ function formatUptime(startedAt) {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Kill any orphaned daemon processes not tracked by PID file.
|
|
121
|
+
* Prevents zombie daemons from racing the real daemon for tasks.
|
|
122
|
+
*/
|
|
123
|
+
function killOrphanedDaemons() {
|
|
124
|
+
try {
|
|
125
|
+
const trackedPid = existsSync(PID_FILE)
|
|
126
|
+
? parseInt(readFileSync(PID_FILE, 'utf8').trim(), 10)
|
|
127
|
+
: null;
|
|
128
|
+
const output = execFileSync('pgrep', ['-f', 'push-todo.*daemon\\.js'], {
|
|
129
|
+
encoding: 'utf8',
|
|
130
|
+
timeout: 5000
|
|
131
|
+
}).trim();
|
|
132
|
+
for (const line of output.split('\n')) {
|
|
133
|
+
const pid = parseInt(line.trim(), 10);
|
|
134
|
+
if (!isNaN(pid) && pid !== trackedPid && pid !== process.pid) {
|
|
135
|
+
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// pgrep not found or no matches — both fine
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
119
143
|
/**
|
|
120
144
|
* Start the daemon process.
|
|
121
145
|
* Same as Python's start_daemon().
|
|
@@ -126,6 +150,9 @@ export function startDaemon() {
|
|
|
126
150
|
return true;
|
|
127
151
|
}
|
|
128
152
|
|
|
153
|
+
// Kill any zombie daemons before starting fresh
|
|
154
|
+
killOrphanedDaemons();
|
|
155
|
+
|
|
129
156
|
mkdirSync(PUSH_DIR, { recursive: true });
|
|
130
157
|
|
|
131
158
|
const daemonScript = join(__dirname, 'daemon.js');
|
|
@@ -141,6 +168,7 @@ export function startDaemon() {
|
|
|
141
168
|
env: (() => {
|
|
142
169
|
const env = { ...process.env, PUSH_DAEMON: '1' };
|
|
143
170
|
delete env.CLAUDECODE; // Strip to avoid leaking into Claude child processes
|
|
171
|
+
delete env.CLAUDE_CODE_ENTRYPOINT; // Strip to avoid any entrypoint-based guards
|
|
144
172
|
return env;
|
|
145
173
|
})()
|
|
146
174
|
});
|
package/lib/daemon.js
CHANGED
|
@@ -78,6 +78,7 @@ const PID_FILE = join(PUSH_DIR, 'daemon.pid');
|
|
|
78
78
|
const LOG_FILE = join(PUSH_DIR, 'daemon.log');
|
|
79
79
|
const STATUS_FILE = join(PUSH_DIR, 'daemon_status.json');
|
|
80
80
|
const VERSION_FILE = join(PUSH_DIR, 'daemon.version');
|
|
81
|
+
const LOCK_FILE = join(PUSH_DIR, 'daemon.lock');
|
|
81
82
|
const CONFIG_FILE = join(CONFIG_DIR, 'config');
|
|
82
83
|
const MACHINE_ID_FILE = join(CONFIG_DIR, 'machine_id');
|
|
83
84
|
const REGISTRY_FILE = join(CONFIG_DIR, 'projects.json');
|
|
@@ -90,8 +91,17 @@ const LOG_BACKUP_COUNT = 3;
|
|
|
90
91
|
const runningTasks = new Map(); // displayNumber -> taskInfo
|
|
91
92
|
const taskDetails = new Map(); // displayNumber -> details
|
|
92
93
|
const completedToday = [];
|
|
94
|
+
const COMPLETED_TODAY_MAX = 100;
|
|
95
|
+
|
|
96
|
+
function trackCompleted(entry) {
|
|
97
|
+
completedToday.push(entry);
|
|
98
|
+
if (completedToday.length > COMPLETED_TODAY_MAX) {
|
|
99
|
+
completedToday.splice(0, completedToday.length - COMPLETED_TODAY_MAX);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
93
102
|
const taskLastOutput = new Map(); // displayNumber -> timestamp
|
|
94
103
|
const taskStdoutBuffer = new Map(); // displayNumber -> lines[]
|
|
104
|
+
const taskStderrBuffer = new Map(); // displayNumber -> lines[]
|
|
95
105
|
const taskProjectPaths = new Map(); // displayNumber -> projectPath
|
|
96
106
|
let daemonStartTime = null;
|
|
97
107
|
|
|
@@ -125,7 +135,7 @@ function rotateLogs() {
|
|
|
125
135
|
|
|
126
136
|
function log(message, level = 'INFO') {
|
|
127
137
|
const timestamp = new Date().toISOString();
|
|
128
|
-
const line = `[${timestamp}] [${level}] ${message}\n`;
|
|
138
|
+
const line = `[${timestamp}] [PID:${process.pid}] [${level}] ${message}\n`;
|
|
129
139
|
|
|
130
140
|
if (process.env.PUSH_DAEMON !== '1') {
|
|
131
141
|
process.stdout.write(line);
|
|
@@ -140,6 +150,38 @@ function logError(message) {
|
|
|
140
150
|
log(message, 'ERROR');
|
|
141
151
|
}
|
|
142
152
|
|
|
153
|
+
// ==================== Lock File ====================
|
|
154
|
+
|
|
155
|
+
function isProcessRunning(pid) {
|
|
156
|
+
try {
|
|
157
|
+
process.kill(pid, 0);
|
|
158
|
+
return true;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function acquireLock() {
|
|
165
|
+
try {
|
|
166
|
+
if (existsSync(LOCK_FILE)) {
|
|
167
|
+
const holderPid = parseInt(readFileSync(LOCK_FILE, 'utf8').trim(), 10);
|
|
168
|
+
if (!isNaN(holderPid) && holderPid !== process.pid && isProcessRunning(holderPid)) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
// Holder is dead or is us — clean up stale lock
|
|
172
|
+
try { unlinkSync(LOCK_FILE); } catch {}
|
|
173
|
+
}
|
|
174
|
+
writeFileSync(LOCK_FILE, String(process.pid));
|
|
175
|
+
return true;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function releaseLock() {
|
|
182
|
+
try { unlinkSync(LOCK_FILE); } catch {}
|
|
183
|
+
}
|
|
184
|
+
|
|
143
185
|
// ==================== Mac Notifications ====================
|
|
144
186
|
|
|
145
187
|
function sendMacNotification(title, message, sound = 'default') {
|
|
@@ -1024,7 +1066,7 @@ async function autoHealExistingWork(displayNumber, summary, projectPath, taskId)
|
|
|
1024
1066
|
}
|
|
1025
1067
|
}
|
|
1026
1068
|
|
|
1027
|
-
|
|
1069
|
+
trackCompleted({
|
|
1028
1070
|
displayNumber, summary,
|
|
1029
1071
|
completedAt: new Date().toISOString(),
|
|
1030
1072
|
duration: 0, status, prUrl
|
|
@@ -1061,7 +1103,7 @@ async function autoHealExistingWork(displayNumber, summary, projectPath, taskId)
|
|
|
1061
1103
|
}
|
|
1062
1104
|
}
|
|
1063
1105
|
|
|
1064
|
-
|
|
1106
|
+
trackCompleted({
|
|
1065
1107
|
displayNumber, summary,
|
|
1066
1108
|
completedAt: new Date().toISOString(),
|
|
1067
1109
|
duration: 0, status, prUrl
|
|
@@ -1078,7 +1120,7 @@ async function autoHealExistingWork(displayNumber, summary, projectPath, taskId)
|
|
|
1078
1120
|
await updateTaskStatus(displayNumber, 'session_finished', {
|
|
1079
1121
|
summary: executionSummary
|
|
1080
1122
|
});
|
|
1081
|
-
|
|
1123
|
+
trackCompleted({
|
|
1082
1124
|
displayNumber, summary,
|
|
1083
1125
|
completedAt: new Date().toISOString(),
|
|
1084
1126
|
duration: 0, status: 'session_finished', prUrl: newPrUrl
|
|
@@ -1380,6 +1422,7 @@ IMPORTANT:
|
|
|
1380
1422
|
env: (() => {
|
|
1381
1423
|
const env = { ...process.env, PUSH_TASK_ID: task.id, PUSH_DISPLAY_NUMBER: String(displayNumber) };
|
|
1382
1424
|
delete env.CLAUDECODE; // Strip to avoid "nested session" guard in Claude Code
|
|
1425
|
+
delete env.CLAUDE_CODE_ENTRYPOINT; // Strip to avoid any entrypoint-based guards
|
|
1383
1426
|
return env;
|
|
1384
1427
|
})()
|
|
1385
1428
|
});
|
|
@@ -1395,6 +1438,20 @@ IMPORTANT:
|
|
|
1395
1438
|
runningTasks.set(displayNumber, taskInfo);
|
|
1396
1439
|
taskLastOutput.set(displayNumber, Date.now());
|
|
1397
1440
|
taskStdoutBuffer.set(displayNumber, []);
|
|
1441
|
+
taskStderrBuffer.set(displayNumber, []);
|
|
1442
|
+
|
|
1443
|
+
// Monitor stderr (critical for diagnosing fast exits)
|
|
1444
|
+
child.stderr.on('data', (data) => {
|
|
1445
|
+
const lines = data.toString().split('\n');
|
|
1446
|
+
for (const line of lines) {
|
|
1447
|
+
if (line.trim()) {
|
|
1448
|
+
const buffer = taskStderrBuffer.get(displayNumber) || [];
|
|
1449
|
+
buffer.push(line);
|
|
1450
|
+
if (buffer.length > 20) buffer.shift();
|
|
1451
|
+
taskStderrBuffer.set(displayNumber, buffer);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1398
1455
|
|
|
1399
1456
|
// Monitor stdout
|
|
1400
1457
|
child.stdout.on('data', (data) => {
|
|
@@ -1569,7 +1626,7 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1569
1626
|
}
|
|
1570
1627
|
}
|
|
1571
1628
|
|
|
1572
|
-
|
|
1629
|
+
trackCompleted({
|
|
1573
1630
|
displayNumber,
|
|
1574
1631
|
summary,
|
|
1575
1632
|
completedAt: new Date().toISOString(),
|
|
@@ -1579,7 +1636,11 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1579
1636
|
sessionId
|
|
1580
1637
|
});
|
|
1581
1638
|
} else {
|
|
1582
|
-
const
|
|
1639
|
+
const stderrLines = taskStderrBuffer.get(displayNumber) || [];
|
|
1640
|
+
const stderr = stderrLines.join('\n');
|
|
1641
|
+
if (stderr) {
|
|
1642
|
+
log(`Task #${displayNumber} stderr: ${stderr.slice(0, 500)}`);
|
|
1643
|
+
}
|
|
1583
1644
|
|
|
1584
1645
|
// Ask Claude to explain what went wrong (needs worktree path)
|
|
1585
1646
|
const failureSummary = extractSemanticSummary(worktreePath, sessionId);
|
|
@@ -1601,7 +1662,7 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1601
1662
|
);
|
|
1602
1663
|
}
|
|
1603
1664
|
|
|
1604
|
-
|
|
1665
|
+
trackCompleted({
|
|
1605
1666
|
displayNumber,
|
|
1606
1667
|
summary,
|
|
1607
1668
|
completedAt: new Date().toISOString(),
|
|
@@ -1614,6 +1675,7 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1614
1675
|
taskDetails.delete(displayNumber);
|
|
1615
1676
|
taskLastOutput.delete(displayNumber);
|
|
1616
1677
|
taskStdoutBuffer.delete(displayNumber);
|
|
1678
|
+
taskStderrBuffer.delete(displayNumber);
|
|
1617
1679
|
taskProjectPaths.delete(displayNumber);
|
|
1618
1680
|
updateStatusFile();
|
|
1619
1681
|
}
|
|
@@ -1744,7 +1806,7 @@ async function checkTimeouts() {
|
|
|
1744
1806
|
);
|
|
1745
1807
|
}
|
|
1746
1808
|
|
|
1747
|
-
|
|
1809
|
+
trackCompleted({
|
|
1748
1810
|
displayNumber,
|
|
1749
1811
|
summary: info.summary || 'Unknown task',
|
|
1750
1812
|
completedAt: new Date().toISOString(),
|
|
@@ -1757,6 +1819,7 @@ async function checkTimeouts() {
|
|
|
1757
1819
|
taskDetails.delete(displayNumber);
|
|
1758
1820
|
taskLastOutput.delete(displayNumber);
|
|
1759
1821
|
taskStdoutBuffer.delete(displayNumber);
|
|
1822
|
+
taskStderrBuffer.delete(displayNumber);
|
|
1760
1823
|
taskProjectPaths.delete(displayNumber);
|
|
1761
1824
|
cleanupWorktree(displayNumber, projectPath);
|
|
1762
1825
|
}
|
|
@@ -1799,6 +1862,7 @@ function checkAndApplyUpdate() {
|
|
|
1799
1862
|
const daemonScript = join(__dirname, 'daemon.js');
|
|
1800
1863
|
const selfUpdateEnv = { ...process.env, PUSH_DAEMON: '1' };
|
|
1801
1864
|
delete selfUpdateEnv.CLAUDECODE; // Strip to avoid leaking into Claude child processes
|
|
1865
|
+
delete selfUpdateEnv.CLAUDE_CODE_ENTRYPOINT; // Strip to avoid any entrypoint-based guards
|
|
1802
1866
|
const child = spawn(process.execPath, [daemonScript], {
|
|
1803
1867
|
detached: true,
|
|
1804
1868
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
@@ -2020,6 +2084,7 @@ async function cleanup() {
|
|
|
2020
2084
|
}
|
|
2021
2085
|
|
|
2022
2086
|
// Clean up files
|
|
2087
|
+
releaseLock();
|
|
2023
2088
|
try { unlinkSync(PID_FILE); } catch {}
|
|
2024
2089
|
|
|
2025
2090
|
// Update status
|
|
@@ -2049,6 +2114,15 @@ mkdirSync(CONFIG_DIR, { recursive: true });
|
|
|
2049
2114
|
// Rotate logs
|
|
2050
2115
|
rotateLogs();
|
|
2051
2116
|
|
|
2117
|
+
// Acquire lock (prevents zombie daemons from racing)
|
|
2118
|
+
if (!acquireLock()) {
|
|
2119
|
+
const holderPid = parseInt(readFileSync(LOCK_FILE, 'utf8').trim(), 10);
|
|
2120
|
+
// Log to file directly since log() may not work yet
|
|
2121
|
+
const msg = `[${new Date().toISOString()}] [PID:${process.pid}] [INFO] Another daemon (PID ${holderPid}) holds the lock. Exiting.\n`;
|
|
2122
|
+
try { appendFileSync(LOG_FILE, msg); } catch {}
|
|
2123
|
+
process.exit(0);
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2052
2126
|
// Write PID file
|
|
2053
2127
|
writeFileSync(PID_FILE, String(process.pid));
|
|
2054
2128
|
|