@masslessai/push-todo 4.1.6 → 4.1.8
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 +294 -19
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -115,6 +115,12 @@ const taskStdoutBuffer = new Map(); // displayNumber -> lines[]
|
|
|
115
115
|
const taskStderrBuffer = new Map(); // displayNumber -> lines[]
|
|
116
116
|
const taskProjectPaths = new Map(); // displayNumber -> projectPath
|
|
117
117
|
const taskLastHeartbeat = new Map(); // displayNumber -> timestamp of last progress heartbeat
|
|
118
|
+
|
|
119
|
+
// Stream-json state (Phase 1: real-time progress)
|
|
120
|
+
const taskStreamLineBuffer = new Map(); // displayNumber -> partial NDJSON line fragment
|
|
121
|
+
const taskActivityState = new Map(); // displayNumber -> { filesRead: Set, filesEdited: Set, currentTool: string, lastText: string }
|
|
122
|
+
const taskLastStreamProgress = new Map(); // displayNumber -> timestamp of last stream progress sent
|
|
123
|
+
const STREAM_PROGRESS_THROTTLE_MS = 30000; // 30s between stream progress updates to Supabase
|
|
118
124
|
let daemonStartTime = null;
|
|
119
125
|
|
|
120
126
|
// ==================== Utilities ====================
|
|
@@ -1332,6 +1338,182 @@ function checkStuckPatterns(displayNumber, line) {
|
|
|
1332
1338
|
return null;
|
|
1333
1339
|
}
|
|
1334
1340
|
|
|
1341
|
+
// ==================== Stream-JSON Parsing (Phase 1) ====================
|
|
1342
|
+
|
|
1343
|
+
function parseStreamJsonLine(line) {
|
|
1344
|
+
const trimmed = line.trim();
|
|
1345
|
+
if (!trimmed || !trimmed.startsWith('{')) return null;
|
|
1346
|
+
try {
|
|
1347
|
+
return JSON.parse(trimmed);
|
|
1348
|
+
} catch {
|
|
1349
|
+
return null;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function extractTextFromContent(content) {
|
|
1354
|
+
if (!Array.isArray(content)) return '';
|
|
1355
|
+
return content
|
|
1356
|
+
.filter(block => block.type === 'text' && block.text)
|
|
1357
|
+
.map(block => block.text)
|
|
1358
|
+
.join('\n');
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function extractToolCallsFromContent(content) {
|
|
1362
|
+
if (!Array.isArray(content)) return [];
|
|
1363
|
+
return content
|
|
1364
|
+
.filter(block => block.type === 'tool_use')
|
|
1365
|
+
.map(block => ({ name: block.name, input: block.input || {} }));
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function processStreamEvent(displayNumber, event) {
|
|
1369
|
+
if (!event || !event.type) return;
|
|
1370
|
+
|
|
1371
|
+
const activity = taskActivityState.get(displayNumber) || {
|
|
1372
|
+
filesRead: new Set(),
|
|
1373
|
+
filesEdited: new Set(),
|
|
1374
|
+
currentTool: null,
|
|
1375
|
+
lastText: '',
|
|
1376
|
+
sessionId: null
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
// Extract session_id from system init or result messages
|
|
1380
|
+
if (event.type === 'system' && event.session_id) {
|
|
1381
|
+
activity.sessionId = event.session_id;
|
|
1382
|
+
}
|
|
1383
|
+
if (event.type === 'result' && event.session_id) {
|
|
1384
|
+
activity.sessionId = event.session_id;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Extract activity from assistant messages
|
|
1388
|
+
if (event.type === 'assistant') {
|
|
1389
|
+
const content = event.message?.content;
|
|
1390
|
+
if (!content) { taskActivityState.set(displayNumber, activity); return; }
|
|
1391
|
+
|
|
1392
|
+
const text = extractTextFromContent(content);
|
|
1393
|
+
if (text) {
|
|
1394
|
+
activity.lastText = text.slice(0, 200);
|
|
1395
|
+
|
|
1396
|
+
// Run existing text-based checks on extracted content
|
|
1397
|
+
if (text.includes('[push-confirm] Waiting for')) {
|
|
1398
|
+
updateTaskDetail(displayNumber, {
|
|
1399
|
+
phase: 'awaiting_confirmation',
|
|
1400
|
+
detail: 'Waiting for user confirmation on iPhone'
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const stuckReason = checkStuckPatterns(displayNumber, text);
|
|
1405
|
+
if (stuckReason) {
|
|
1406
|
+
log(`Task #${displayNumber} may be stuck: ${stuckReason}`);
|
|
1407
|
+
updateTaskDetail(displayNumber, {
|
|
1408
|
+
phase: 'stuck',
|
|
1409
|
+
detail: `Waiting for input: ${stuckReason}`
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const toolCalls = extractToolCallsFromContent(content);
|
|
1415
|
+
for (const tool of toolCalls) {
|
|
1416
|
+
activity.currentTool = tool.name;
|
|
1417
|
+
const filePath = tool.input?.file_path || tool.input?.path;
|
|
1418
|
+
if (filePath) {
|
|
1419
|
+
if (tool.name === 'Read') {
|
|
1420
|
+
activity.filesRead.add(filePath);
|
|
1421
|
+
} else if (tool.name === 'Edit' || tool.name === 'Write') {
|
|
1422
|
+
activity.filesEdited.add(filePath);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
taskActivityState.set(displayNumber, activity);
|
|
1429
|
+
|
|
1430
|
+
// Throttled progress reporting to Supabase
|
|
1431
|
+
maybesSendStreamProgress(displayNumber, activity);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function maybesSendStreamProgress(displayNumber, activity) {
|
|
1435
|
+
const now = Date.now();
|
|
1436
|
+
const lastSent = taskLastStreamProgress.get(displayNumber) || 0;
|
|
1437
|
+
if (now - lastSent < STREAM_PROGRESS_THROTTLE_MS) return;
|
|
1438
|
+
taskLastStreamProgress.set(displayNumber, now);
|
|
1439
|
+
|
|
1440
|
+
const info = taskDetails.get(displayNumber) || {};
|
|
1441
|
+
const taskId = info.taskId || null;
|
|
1442
|
+
const taskInfo = runningTasks.get(displayNumber);
|
|
1443
|
+
if (!taskInfo) return;
|
|
1444
|
+
|
|
1445
|
+
const elapsedSec = Math.floor((now - taskInfo.startTime) / 1000);
|
|
1446
|
+
const elapsedMin = Math.floor(elapsedSec / 60);
|
|
1447
|
+
|
|
1448
|
+
// Build a meaningful summary from stream activity
|
|
1449
|
+
const parts = [`Running for ${elapsedMin}m.`];
|
|
1450
|
+
if (activity.filesEdited.size > 0) {
|
|
1451
|
+
const editedList = [...activity.filesEdited].map(f => f.split('/').pop()).slice(-5);
|
|
1452
|
+
parts.push(`Edited: ${editedList.join(', ')}`);
|
|
1453
|
+
}
|
|
1454
|
+
if (activity.filesRead.size > 0) {
|
|
1455
|
+
parts.push(`Read ${activity.filesRead.size} files.`);
|
|
1456
|
+
}
|
|
1457
|
+
if (activity.currentTool) {
|
|
1458
|
+
parts.push(`Current: ${activity.currentTool}`);
|
|
1459
|
+
}
|
|
1460
|
+
if (activity.lastText && !activity.currentTool) {
|
|
1461
|
+
parts.push(activity.lastText.slice(0, 80));
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const eventSummary = parts.join(' ');
|
|
1465
|
+
|
|
1466
|
+
log(`Task #${displayNumber}: stream progress (${eventSummary})`);
|
|
1467
|
+
|
|
1468
|
+
// Also update the existing heartbeat timestamp to prevent duplicate generic heartbeats
|
|
1469
|
+
taskLastHeartbeat.set(displayNumber, now);
|
|
1470
|
+
|
|
1471
|
+
apiRequest('update-task-execution', {
|
|
1472
|
+
method: 'PATCH',
|
|
1473
|
+
body: JSON.stringify({
|
|
1474
|
+
todoId: taskId,
|
|
1475
|
+
displayNumber,
|
|
1476
|
+
event: {
|
|
1477
|
+
type: 'progress',
|
|
1478
|
+
timestamp: new Date().toISOString(),
|
|
1479
|
+
machineName: getMachineName() || undefined,
|
|
1480
|
+
summary: eventSummary,
|
|
1481
|
+
filesEdited: [...activity.filesEdited].slice(-10),
|
|
1482
|
+
filesRead: activity.filesRead.size
|
|
1483
|
+
}
|
|
1484
|
+
})
|
|
1485
|
+
}).then(async (response) => {
|
|
1486
|
+
// Check for cancellation signal in response
|
|
1487
|
+
const result = await response.json().catch(() => null);
|
|
1488
|
+
if (result?.status === 'cancelling') {
|
|
1489
|
+
handleCancellation(displayNumber, 'stream_progress');
|
|
1490
|
+
}
|
|
1491
|
+
}).catch(err => {
|
|
1492
|
+
log(`Task #${displayNumber}: stream progress failed (non-fatal): ${err.message}`);
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// ==================== Cancellation (Phase 2) ====================
|
|
1497
|
+
|
|
1498
|
+
function handleCancellation(displayNumber, source) {
|
|
1499
|
+
const taskInfo = runningTasks.get(displayNumber);
|
|
1500
|
+
if (!taskInfo) return;
|
|
1501
|
+
|
|
1502
|
+
log(`Task #${displayNumber}: cancellation detected via ${source}, sending SIGTERM`);
|
|
1503
|
+
|
|
1504
|
+
try {
|
|
1505
|
+
taskInfo.process.kill('SIGTERM');
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
log(`Task #${displayNumber}: SIGTERM failed: ${err.message}`);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// Mark as cancelled so handleTaskCompletion reports the right reason
|
|
1511
|
+
updateTaskDetail(displayNumber, {
|
|
1512
|
+
phase: 'cancelled',
|
|
1513
|
+
detail: 'Cancelled by user'
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1335
1517
|
function monitorTaskStdout(displayNumber, proc) {
|
|
1336
1518
|
if (!proc.stdout) return;
|
|
1337
1519
|
|
|
@@ -1429,7 +1611,23 @@ async function sendProgressHeartbeats() {
|
|
|
1429
1611
|
|
|
1430
1612
|
const activityDesc = idleSec < 60 ? 'active' : `idle ${idleMin}m`;
|
|
1431
1613
|
const phase = info.phase || 'executing';
|
|
1432
|
-
|
|
1614
|
+
|
|
1615
|
+
// Enrich heartbeat with stream activity data when available
|
|
1616
|
+
const activity = taskActivityState.get(displayNumber);
|
|
1617
|
+
const parts = [`Running for ${elapsedMin}m. Last activity: ${activityDesc}. Phase: ${phase}.`];
|
|
1618
|
+
const eventExtra = {};
|
|
1619
|
+
if (activity) {
|
|
1620
|
+
if (activity.filesEdited.size > 0) {
|
|
1621
|
+
const editedList = [...activity.filesEdited].map(f => f.split('/').pop()).slice(-5);
|
|
1622
|
+
parts.push(`Edited: ${editedList.join(', ')}`);
|
|
1623
|
+
eventExtra.filesEdited = [...activity.filesEdited].slice(-10);
|
|
1624
|
+
}
|
|
1625
|
+
if (activity.filesRead.size > 0) {
|
|
1626
|
+
parts.push(`Read ${activity.filesRead.size} files.`);
|
|
1627
|
+
eventExtra.filesRead = activity.filesRead.size;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
const eventSummary = parts.join(' ');
|
|
1433
1631
|
|
|
1434
1632
|
log(`Task #${displayNumber}: sending progress heartbeat (${eventSummary})`);
|
|
1435
1633
|
|
|
@@ -1447,10 +1645,17 @@ async function sendProgressHeartbeats() {
|
|
|
1447
1645
|
type: 'progress',
|
|
1448
1646
|
timestamp: new Date().toISOString(),
|
|
1449
1647
|
machineName: getMachineName() || undefined,
|
|
1450
|
-
summary: eventSummary
|
|
1648
|
+
summary: eventSummary,
|
|
1649
|
+
...eventExtra
|
|
1451
1650
|
}
|
|
1452
1651
|
// No status field — event-only update, won't change execution_status
|
|
1453
1652
|
})
|
|
1653
|
+
}).then(async (response) => {
|
|
1654
|
+
// Check for cancellation signal in heartbeat response
|
|
1655
|
+
const result = await response.json().catch(() => null);
|
|
1656
|
+
if (result?.status === 'cancelling') {
|
|
1657
|
+
handleCancellation(displayNumber, 'heartbeat');
|
|
1658
|
+
}
|
|
1454
1659
|
}).catch(err => {
|
|
1455
1660
|
log(`Task #${displayNumber}: heartbeat failed (non-fatal): ${err.message}`);
|
|
1456
1661
|
});
|
|
@@ -1543,6 +1748,9 @@ async function killIdleTasks() {
|
|
|
1543
1748
|
taskStderrBuffer.delete(displayNumber);
|
|
1544
1749
|
taskProjectPaths.delete(displayNumber);
|
|
1545
1750
|
taskLastHeartbeat.delete(displayNumber);
|
|
1751
|
+
taskStreamLineBuffer.delete(displayNumber);
|
|
1752
|
+
taskActivityState.delete(displayNumber);
|
|
1753
|
+
taskLastStreamProgress.delete(displayNumber);
|
|
1546
1754
|
}
|
|
1547
1755
|
|
|
1548
1756
|
if (idleTimedOut.length > 0) {
|
|
@@ -1552,7 +1760,14 @@ async function killIdleTasks() {
|
|
|
1552
1760
|
|
|
1553
1761
|
// ==================== Session ID Extraction ====================
|
|
1554
1762
|
|
|
1555
|
-
function extractSessionIdFromStdout(proc, buffer) {
|
|
1763
|
+
function extractSessionIdFromStdout(proc, buffer, displayNumber) {
|
|
1764
|
+
// First: check stream activity state (populated by stream-json parsing)
|
|
1765
|
+
if (displayNumber != null) {
|
|
1766
|
+
const activity = taskActivityState.get(displayNumber);
|
|
1767
|
+
if (activity?.sessionId) return activity.sessionId;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// Fallback: drain remaining stdout and scan for session_id in JSON lines
|
|
1556
1771
|
let remaining = '';
|
|
1557
1772
|
if (proc.stdout) {
|
|
1558
1773
|
try {
|
|
@@ -1562,7 +1777,6 @@ function extractSessionIdFromStdout(proc, buffer) {
|
|
|
1562
1777
|
|
|
1563
1778
|
const allOutput = buffer.join('\n') + '\n' + remaining;
|
|
1564
1779
|
|
|
1565
|
-
// Try to parse JSON output
|
|
1566
1780
|
for (const line of allOutput.split('\n')) {
|
|
1567
1781
|
const trimmed = line.trim();
|
|
1568
1782
|
if (trimmed.startsWith('{') && trimmed.includes('session_id')) {
|
|
@@ -1827,14 +2041,14 @@ async function executeTask(task) {
|
|
|
1827
2041
|
'--continue', previousSessionId,
|
|
1828
2042
|
'-p', prompt,
|
|
1829
2043
|
'--allowedTools', allowedTools,
|
|
1830
|
-
'--output-format', 'json',
|
|
2044
|
+
'--output-format', 'stream-json',
|
|
1831
2045
|
'--permission-mode', 'bypassPermissions',
|
|
1832
2046
|
'--session-id', preAssignedSessionId
|
|
1833
2047
|
]
|
|
1834
2048
|
: [
|
|
1835
2049
|
'-p', prompt,
|
|
1836
2050
|
'--allowedTools', allowedTools,
|
|
1837
|
-
'--output-format', 'json',
|
|
2051
|
+
'--output-format', 'stream-json',
|
|
1838
2052
|
'--permission-mode', 'bypassPermissions',
|
|
1839
2053
|
'--session-id', preAssignedSessionId
|
|
1840
2054
|
];
|
|
@@ -1883,25 +2097,44 @@ async function executeTask(task) {
|
|
|
1883
2097
|
}
|
|
1884
2098
|
});
|
|
1885
2099
|
|
|
1886
|
-
// Monitor stdout
|
|
2100
|
+
// Monitor stdout (stream-json NDJSON parsing)
|
|
2101
|
+
taskStreamLineBuffer.set(displayNumber, '');
|
|
2102
|
+
taskActivityState.set(displayNumber, {
|
|
2103
|
+
filesRead: new Set(), filesEdited: new Set(),
|
|
2104
|
+
currentTool: null, lastText: '', sessionId: null
|
|
2105
|
+
});
|
|
2106
|
+
taskLastStreamProgress.set(displayNumber, Date.now());
|
|
2107
|
+
|
|
1887
2108
|
child.stdout.on('data', (data) => {
|
|
1888
|
-
|
|
2109
|
+
taskLastOutput.set(displayNumber, Date.now());
|
|
2110
|
+
|
|
2111
|
+
// NDJSON line buffering: handle chunks that split across line boundaries
|
|
2112
|
+
const pending = (taskStreamLineBuffer.get(displayNumber) || '') + data.toString();
|
|
2113
|
+
const lines = pending.split('\n');
|
|
2114
|
+
// Last element is either empty (chunk ended with \n) or a partial line
|
|
2115
|
+
taskStreamLineBuffer.set(displayNumber, lines.pop() || '');
|
|
2116
|
+
|
|
1889
2117
|
for (const line of lines) {
|
|
1890
|
-
if (line.trim())
|
|
1891
|
-
taskLastOutput.set(displayNumber, Date.now());
|
|
1892
|
-
const buffer = taskStdoutBuffer.get(displayNumber) || [];
|
|
1893
|
-
buffer.push(line);
|
|
1894
|
-
if (buffer.length > 20) buffer.shift();
|
|
1895
|
-
taskStdoutBuffer.set(displayNumber, buffer);
|
|
2118
|
+
if (!line.trim()) continue;
|
|
1896
2119
|
|
|
1897
|
-
|
|
2120
|
+
// Keep raw lines in circular buffer for debugging/fallback
|
|
2121
|
+
const buffer = taskStdoutBuffer.get(displayNumber) || [];
|
|
2122
|
+
buffer.push(line);
|
|
2123
|
+
if (buffer.length > 50) buffer.shift();
|
|
2124
|
+
taskStdoutBuffer.set(displayNumber, buffer);
|
|
2125
|
+
|
|
2126
|
+
// Parse as NDJSON stream event
|
|
2127
|
+
const event = parseStreamJsonLine(line);
|
|
2128
|
+
if (event) {
|
|
2129
|
+
processStreamEvent(displayNumber, event);
|
|
2130
|
+
} else {
|
|
2131
|
+
// Fallback: line isn't valid JSON — run legacy text checks
|
|
1898
2132
|
if (line.includes('[push-confirm] Waiting for')) {
|
|
1899
2133
|
updateTaskDetail(displayNumber, {
|
|
1900
2134
|
phase: 'awaiting_confirmation',
|
|
1901
2135
|
detail: 'Waiting for user confirmation on iPhone'
|
|
1902
2136
|
});
|
|
1903
2137
|
}
|
|
1904
|
-
|
|
1905
2138
|
const stuckReason = checkStuckPatterns(displayNumber, line);
|
|
1906
2139
|
if (stuckReason) {
|
|
1907
2140
|
log(`Task #${displayNumber} may be stuck: ${stuckReason}`);
|
|
@@ -1925,6 +2158,9 @@ async function executeTask(task) {
|
|
|
1925
2158
|
await updateTaskStatus(displayNumber, 'failed', { error: error.message }, taskId);
|
|
1926
2159
|
taskDetails.delete(displayNumber);
|
|
1927
2160
|
taskLastHeartbeat.delete(displayNumber);
|
|
2161
|
+
taskStreamLineBuffer.delete(displayNumber);
|
|
2162
|
+
taskActivityState.delete(displayNumber);
|
|
2163
|
+
taskLastStreamProgress.delete(displayNumber);
|
|
1928
2164
|
updateStatusFile();
|
|
1929
2165
|
});
|
|
1930
2166
|
|
|
@@ -1941,6 +2177,9 @@ async function executeTask(task) {
|
|
|
1941
2177
|
logError(`Error starting Claude for task #${displayNumber}: ${error.message}`);
|
|
1942
2178
|
await updateTaskStatus(displayNumber, 'failed', { error: error.message }, taskId);
|
|
1943
2179
|
taskDetails.delete(displayNumber);
|
|
2180
|
+
taskStreamLineBuffer.delete(displayNumber);
|
|
2181
|
+
taskActivityState.delete(displayNumber);
|
|
2182
|
+
taskLastStreamProgress.delete(displayNumber);
|
|
1944
2183
|
return null;
|
|
1945
2184
|
}
|
|
1946
2185
|
}
|
|
@@ -1956,12 +2195,45 @@ async function handleTaskCompletion(displayNumber, exitCode) {
|
|
|
1956
2195
|
const summary = info.summary || 'Unknown task';
|
|
1957
2196
|
const projectPath = taskProjectPaths.get(displayNumber);
|
|
1958
2197
|
|
|
1959
|
-
|
|
2198
|
+
const durationStr = duration < 60 ? `${duration}s` : `${Math.floor(duration / 60)}m ${duration % 60}s`;
|
|
2199
|
+
const wasCancelled = info.phase === 'cancelled';
|
|
2200
|
+
log(`Task #${displayNumber} ${wasCancelled ? 'cancelled' : 'completed'} with code ${exitCode} (${duration}s)`);
|
|
2201
|
+
|
|
2202
|
+
// Handle user cancellation — report as failed with clear reason, skip PR/merge/summary
|
|
2203
|
+
if (wasCancelled) {
|
|
2204
|
+
const cancelMsg = `Cancelled by user after ${durationStr}.`;
|
|
2205
|
+
await updateTaskStatus(displayNumber, 'failed', {
|
|
2206
|
+
error: cancelMsg,
|
|
2207
|
+
sessionId: taskInfo.sessionId
|
|
2208
|
+
}, info.taskId);
|
|
2209
|
+
|
|
2210
|
+
cleanupWorktree(displayNumber, projectPath);
|
|
2211
|
+
|
|
2212
|
+
trackCompleted({
|
|
2213
|
+
displayNumber,
|
|
2214
|
+
summary,
|
|
2215
|
+
completedAt: new Date().toISOString(),
|
|
2216
|
+
duration,
|
|
2217
|
+
status: 'cancelled'
|
|
2218
|
+
});
|
|
2219
|
+
|
|
2220
|
+
// Cleanup internal tracking
|
|
2221
|
+
taskDetails.delete(displayNumber);
|
|
2222
|
+
taskLastOutput.delete(displayNumber);
|
|
2223
|
+
taskStdoutBuffer.delete(displayNumber);
|
|
2224
|
+
taskStderrBuffer.delete(displayNumber);
|
|
2225
|
+
taskProjectPaths.delete(displayNumber);
|
|
2226
|
+
taskLastHeartbeat.delete(displayNumber);
|
|
2227
|
+
taskStreamLineBuffer.delete(displayNumber);
|
|
2228
|
+
taskActivityState.delete(displayNumber);
|
|
2229
|
+
taskLastStreamProgress.delete(displayNumber);
|
|
2230
|
+
updateStatusFile();
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
1960
2233
|
|
|
1961
2234
|
// Session ID: prefer pre-assigned ID (reliable), fall back to stdout extraction
|
|
1962
|
-
const sessionId = taskInfo.sessionId || extractSessionIdFromStdout(taskInfo.process, taskStdoutBuffer.get(displayNumber) || []);
|
|
2235
|
+
const sessionId = taskInfo.sessionId || extractSessionIdFromStdout(taskInfo.process, taskStdoutBuffer.get(displayNumber) || [], displayNumber);
|
|
1963
2236
|
const worktreePath = getWorktreePath(displayNumber, projectPath);
|
|
1964
|
-
const durationStr = duration < 60 ? `${duration}s` : `${Math.floor(duration / 60)}m ${duration % 60}s`;
|
|
1965
2237
|
const machineName = getMachineName() || 'Mac';
|
|
1966
2238
|
|
|
1967
2239
|
if (sessionId) {
|
|
@@ -2301,6 +2573,9 @@ async function checkTimeouts() {
|
|
|
2301
2573
|
taskStderrBuffer.delete(displayNumber);
|
|
2302
2574
|
taskProjectPaths.delete(displayNumber);
|
|
2303
2575
|
taskLastHeartbeat.delete(displayNumber);
|
|
2576
|
+
taskStreamLineBuffer.delete(displayNumber);
|
|
2577
|
+
taskActivityState.delete(displayNumber);
|
|
2578
|
+
taskLastStreamProgress.delete(displayNumber);
|
|
2304
2579
|
cleanupWorktree(displayNumber, projectPath);
|
|
2305
2580
|
}
|
|
2306
2581
|
|