@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 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'
@@ -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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masslessai/push-todo",
3
- "version": "4.2.4",
3
+ "version": "4.2.5",
4
4
  "description": "Voice tasks from Push iOS app for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {