@masslessai/push-todo 4.2.4 → 4.2.5
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.js +44 -0
- package/lib/launchagent.js +1 -1
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -91,6 +91,7 @@ const LOG_FILE = join(PUSH_DIR, 'daemon.log');
|
|
|
91
91
|
const STATUS_FILE = join(PUSH_DIR, 'daemon_status.json');
|
|
92
92
|
const VERSION_FILE = join(PUSH_DIR, 'daemon.version');
|
|
93
93
|
const LOCK_FILE = join(PUSH_DIR, 'daemon.lock');
|
|
94
|
+
const COMPLETED_FILE = join(PUSH_DIR, 'completed_tasks.json');
|
|
94
95
|
const CONFIG_FILE = join(CONFIG_DIR, 'config');
|
|
95
96
|
const MACHINE_ID_FILE = join(CONFIG_DIR, 'machine_id');
|
|
96
97
|
const REGISTRY_FILE = join(CONFIG_DIR, 'projects.json');
|
|
@@ -110,6 +111,25 @@ function trackCompleted(entry) {
|
|
|
110
111
|
if (completedToday.length > COMPLETED_TODAY_MAX) {
|
|
111
112
|
completedToday.splice(0, completedToday.length - COMPLETED_TODAY_MAX);
|
|
112
113
|
}
|
|
114
|
+
// Persist to disk so new daemon instances skip already-completed tasks
|
|
115
|
+
try {
|
|
116
|
+
writeFileSync(COMPLETED_FILE, JSON.stringify(completedToday));
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function loadCompletedTasks() {
|
|
121
|
+
try {
|
|
122
|
+
if (!existsSync(COMPLETED_FILE)) return;
|
|
123
|
+
const data = JSON.parse(readFileSync(COMPLETED_FILE, 'utf8'));
|
|
124
|
+
if (!Array.isArray(data)) return;
|
|
125
|
+
// Only load entries from the last 24 hours
|
|
126
|
+
const cutoff = Date.now() - 86400000;
|
|
127
|
+
for (const entry of data) {
|
|
128
|
+
if (entry.completedAt && new Date(entry.completedAt).getTime() > cutoff) {
|
|
129
|
+
completedToday.push(entry);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {}
|
|
113
133
|
}
|
|
114
134
|
const taskLastOutput = new Map(); // displayNumber -> timestamp
|
|
115
135
|
const taskStdoutBuffer = new Map(); // displayNumber -> lines[]
|
|
@@ -886,6 +906,16 @@ function createPRForTask(displayNumber, summary, projectPath) {
|
|
|
886
906
|
const gitCwd = projectPath || process.cwd();
|
|
887
907
|
|
|
888
908
|
try {
|
|
909
|
+
// Verify branch exists before comparing (worktree may have been auto-cleaned)
|
|
910
|
+
try {
|
|
911
|
+
execFileSync('git', ['rev-parse', '--verify', branch], {
|
|
912
|
+
cwd: gitCwd, timeout: 5000, stdio: 'pipe'
|
|
913
|
+
});
|
|
914
|
+
} catch {
|
|
915
|
+
log(`Branch ${branch} does not exist, skipping PR creation`);
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
|
|
889
919
|
// Check if branch has commits
|
|
890
920
|
const logResult = execSync(`git log HEAD..${branch} --oneline`, {
|
|
891
921
|
cwd: gitCwd,
|
|
@@ -1667,6 +1697,7 @@ function respawnWithInjectedMessage(displayNumber) {
|
|
|
1667
1697
|
'--continue', sessionId,
|
|
1668
1698
|
'-p', injectionPrompt,
|
|
1669
1699
|
'--verbose',
|
|
1700
|
+
'--worktree', getWorktreeName(displayNumber),
|
|
1670
1701
|
'--allowedTools', allowedTools,
|
|
1671
1702
|
'--output-format', 'stream-json',
|
|
1672
1703
|
'--permission-mode', 'bypassPermissions',
|
|
@@ -2332,6 +2363,7 @@ async function executeTask(task) {
|
|
|
2332
2363
|
'--continue', previousSessionId,
|
|
2333
2364
|
'-p', prompt,
|
|
2334
2365
|
'--verbose',
|
|
2366
|
+
'--worktree', worktreeName,
|
|
2335
2367
|
'--allowedTools', allowedTools,
|
|
2336
2368
|
'--output-format', 'stream-json',
|
|
2337
2369
|
'--permission-mode', 'bypassPermissions',
|
|
@@ -2340,6 +2372,7 @@ async function executeTask(task) {
|
|
|
2340
2372
|
: [
|
|
2341
2373
|
'-p', prompt,
|
|
2342
2374
|
'--verbose',
|
|
2375
|
+
'--worktree', worktreeName,
|
|
2343
2376
|
'--allowedTools', allowedTools,
|
|
2344
2377
|
'--output-format', 'stream-json',
|
|
2345
2378
|
'--permission-mode', 'bypassPermissions',
|
|
@@ -3200,6 +3233,14 @@ async function recoverOrphanedTasks() {
|
|
|
3200
3233
|
for (const task of orphaned) {
|
|
3201
3234
|
const dn = task.displayNumber || task.display_number;
|
|
3202
3235
|
const tid = task.id || task.todo_id || null;
|
|
3236
|
+
|
|
3237
|
+
// Skip tasks already completed by the previous daemon instance
|
|
3238
|
+
// (race condition: API may return stale 'running' status after session_finished update)
|
|
3239
|
+
if (completedToday.some(c => c.displayNumber === dn)) {
|
|
3240
|
+
log(`Task #${dn}: skipping orphan recovery — already completed by previous daemon`);
|
|
3241
|
+
continue;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3203
3244
|
log(`Task #${dn}: resetting from 'running' to 'queued' (orphaned by restart)`);
|
|
3204
3245
|
await updateTaskStatus(dn, 'queued', {
|
|
3205
3246
|
event: {
|
|
@@ -3259,6 +3300,9 @@ async function mainLoop() {
|
|
|
3259
3300
|
writeFileSync(VERSION_FILE, getVersion());
|
|
3260
3301
|
} catch {}
|
|
3261
3302
|
|
|
3303
|
+
// Load completed tasks from previous daemon instance (prevents re-execution)
|
|
3304
|
+
loadCompletedTasks();
|
|
3305
|
+
|
|
3262
3306
|
// Recover orphaned tasks from previous daemon instance
|
|
3263
3307
|
// When the daemon restarts (self-update, crash, reboot), tasks may be stuck
|
|
3264
3308
|
// in 'running' with no process actually working on them. Reset them to 'queued'
|
package/lib/launchagent.js
CHANGED
|
@@ -68,7 +68,7 @@ function generatePlist() {
|
|
|
68
68
|
<key>PUSH_DAEMON</key>
|
|
69
69
|
<string>1</string>
|
|
70
70
|
<key>PATH</key>
|
|
71
|
-
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${dirname(nodeBin)}</string>
|
|
71
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${dirname(nodeBin)}:${join(homedir(), '.local', 'bin')}</string>
|
|
72
72
|
</dict>
|
|
73
73
|
|
|
74
74
|
<key>RunAtLoad</key>
|