Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 37 additions & 37 deletions .github/agent/agent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1229,48 +1229,48 @@ This is ONE FILE ONLY. Generate the complete diff now:`;
if (!filesToModify || allDiffs.length === 0) {
console.log(`[AGENT] Using single-pass generation method...`);
const apiStartTime = Date.now();
try {
output = await retryWithBackoff(async () => {
return await callAI(prompt);
}, 3, 1000);
const apiDuration = Date.now() - apiStartTime;
console.log(`[AGENT] API call completed in ${apiDuration}ms`);
} catch (error) {
const errorDetails = formatErrorDetails(error, {});
await handleError(error, `${provider.toUpperCase()} API Error`, { errorDetails });
// handleError calls process.exit(1), so we never reach here
try {
output = await retryWithBackoff(async () => {
return await callAI(prompt);
}, 3, 1000);
const apiDuration = Date.now() - apiStartTime;
console.log(`[AGENT] API call completed in ${apiDuration}ms`);
} catch (error) {
const errorDetails = formatErrorDetails(error, {});
await handleError(error, `${provider.toUpperCase()} API Error`, { errorDetails });
// handleError calls process.exit(1), so we never reach here
}
}

// Extract diff from potential markdown code blocks (only if using fallback method)
let diff = output.trim();
if (!filesToModify || allDiffs.length === 0) {
// Only do markdown extraction for fallback single-pass method
if (output.includes("```")) {
// Try to extract content between code fences
// Handle both single and multiple code blocks
const matches = output.matchAll(/```(?:diff)?\n([\s\S]*?)```/g);
const extractedDiffs = [];
for (const match of matches) {
extractedDiffs.push(match[1].trim());
}
// Use the longest extracted diff (likely the actual diff)
if (extractedDiffs.length > 0) {
diff = extractedDiffs.reduce((a, b) => a.length > b.length ? a : b);
}
// If no code blocks found but output contains diff markers, use the whole output
if (!diff.includes("--- a/") && output.includes("--- a/")) {
// Extract everything after the first "--- a/" line
const diffStart = output.indexOf("--- a/");
diff = output.substring(diffStart).trim();
// Remove any trailing markdown or explanations
const diffEnd = diff.indexOf("\n\n```") !== -1 ? diff.indexOf("\n\n```") :
diff.indexOf("\n\n##") !== -1 ? diff.indexOf("\n\n##") :
diff.indexOf("\n\n**") !== -1 ? diff.indexOf("\n\n**") :
diff.length;
diff = diff.substring(0, diffEnd).trim();
}
if (output.includes("```")) {
// Try to extract content between code fences
// Handle both single and multiple code blocks
const matches = output.matchAll(/```(?:diff)?\n([\s\S]*?)```/g);
const extractedDiffs = [];
for (const match of matches) {
extractedDiffs.push(match[1].trim());
}
// Use the longest extracted diff (likely the actual diff)
if (extractedDiffs.length > 0) {
diff = extractedDiffs.reduce((a, b) => a.length > b.length ? a : b);
}

// If no code blocks found but output contains diff markers, use the whole output
if (!diff.includes("--- a/") && output.includes("--- a/")) {
// Extract everything after the first "--- a/" line
const diffStart = output.indexOf("--- a/");
diff = output.substring(diffStart).trim();
// Remove any trailing markdown or explanations
const diffEnd = diff.indexOf("\n\n```") !== -1 ? diff.indexOf("\n\n```") :
diff.indexOf("\n\n##") !== -1 ? diff.indexOf("\n\n##") :
diff.indexOf("\n\n**") !== -1 ? diff.indexOf("\n\n**") :
diff.length;
diff = diff.substring(0, diffEnd).trim();
}
}
} else {
// For one-file-at-a-time, diff is already clean (output from combined diffs)
Expand Down Expand Up @@ -1372,9 +1372,9 @@ if (validationResult.errors.length > 0 || diff.match(/\+\+\+ b\/[^\n]*$/m)) {
}

const errorMsg = `${truncationReason}. The AI model may have generated an incomplete diff.\n\nErrors:\n${validationResult.errors.join('\n')}\n\nAPI Response Info:\n- Stop reason: ${stopReason}\n- Output tokens used: ${outputTokens}\n- Was truncated: ${wasTruncated}\n\nDiff preview (last 500 chars):\n\`\`\`\n${diff.substring(Math.max(0, diffLength - 500))}\n\`\`\`\n\nPossible causes:\n- Model hit output token limit (check max_tokens setting)\n- Input context too large, leaving insufficient room for output\n- Model stopped generating for other reasons\n\nSuggestions:\n- Reduce input context size (fewer files) - already reduced to 400KB\n- Break task into smaller parts\n- Verify max_tokens is sufficient (currently 180K)`;
const errorDetails = formatErrorDetails(new Error(errorMsg), { diff: diff.substring(Math.max(0, diffLength - 1000)), files: validationResult.stats.filesChanged });
const errorDetails = formatErrorDetails(new Error(errorMsg), { diff: diff.substring(Math.max(0, diffLength - 1000)), files: validationResult.stats.filesChanged });
await handleError(new Error(errorMsg), "Incomplete Diff (Truncated)", { diff: diff.substring(Math.max(0, diffLength - 1000)), errorDetails });
// handleError calls process.exit(1), so we never reach here
// handleError calls process.exit(1), so we never reach here
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions app.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,7 @@







4 changes: 4 additions & 0 deletions docs/DISCORD_DISCOVERY_CONTENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,7 @@ Join our Discord server for:







93 changes: 84 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2340,15 +2340,15 @@ async function getNowPlaying(options = {}) {
// Only fetch queue if explicitly needed (skip for status polling to avoid choppy playback)
if (!skipQueue) {
promises.push(
sonos.getQueue().catch(err => {
// Ignore queue errors for now-playing
return null;
})
sonos.getQueue().catch(err => {
// Ignore queue errors for now-playing
return null;
})
);
} else {
promises.push(Promise.resolve(null));
}

const [state, volume, queue] = await Promise.all(promises);

// Fetch queue (next tracks) - only if we have queue data
Expand Down Expand Up @@ -4463,9 +4463,18 @@ async function _add(input, channel, userName) {
if (state === 'stopped') {
logger.info('Player stopped - ensuring queue is active source and flushing');
try {
// Parallel stop and flush (flush is safe even if not stopped)
// Stop any active playback to force Sonos to use queue
try {
await sonos.stop();
await new Promise(resolve => setTimeout(resolve, 300));
} catch (stopErr) {
// Ignore stop errors (might already be stopped)
logger.debug('Stop command result (may already be stopped): ' + stopErr.message);
}

// Flush queue to start fresh
await sonos.flush();
await new Promise(resolve => setTimeout(resolve, 200)); // Reduced from 300ms
await new Promise(resolve => setTimeout(resolve, 300));
logger.info('Queue flushed and ready');
} catch (flushErr) {
logger.warn('Could not flush queue: ' + flushErr.message);
Expand Down Expand Up @@ -4493,8 +4502,9 @@ async function _add(input, channel, userName) {
// Try to queue the first valid candidate (most relevant result)
let result = null;
try {
logger.info(`Attempting to queue: ${firstCandidate.name} by ${firstCandidate.artist} (URI: ${firstCandidate.uri})`);
await sonos.queue(firstCandidate.uri);
logger.info('Added track: ' + firstCandidate.name);
logger.info('Successfully queued track: ' + firstCandidate.name);
result = firstCandidate;
} catch (e) {
const errorDetails = e.message || String(e);
Expand Down Expand Up @@ -4540,7 +4550,72 @@ async function _add(input, channel, userName) {
// Start playback in background (don't await)
(async () => {
try {
await new Promise(resolve => setTimeout(resolve, 300)); // Brief delay for queue to settle
// Ensure queue is the active source before starting playback
// Stop any active playback to force Sonos to use queue
try {
await sonos.stop();
await new Promise(resolve => setTimeout(resolve, 500));
} catch (stopErr) {
// Ignore stop errors (might already be stopped)
logger.debug('Stop before play (may already be stopped): ' + stopErr.message);
}

// Verify queue has items before trying to play (prevents UPnP error 701)
let queueReady = false;
let retries = 0;
while (!queueReady && retries < 5) {
try {
const queue = await sonos.getQueue();
if (queue && queue.items && queue.items.length > 0) {
queueReady = true;
logger.debug(`Queue verified: ${queue.items.length} items ready`);
} else {
logger.debug(`Queue not ready yet (attempt ${retries + 1}/5), waiting...`);
await new Promise(resolve => setTimeout(resolve, 300));
retries++;
}
} catch (queueErr) {
logger.debug(`Queue check failed (attempt ${retries + 1}/5): ${queueErr.message}`);
await new Promise(resolve => setTimeout(resolve, 300));
retries++;
}
}

if (!queueReady) {
logger.warn('Queue not ready after 5 attempts, attempting playback anyway');
Comment on lines +4566 to +4585

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The queue verification retry loop attempts to call sonos.getQueue() up to 5 times even though the PR description mentions this was optimized to prevent choppy playback with large queues. This retry mechanism in the playback path may cause performance issues with queues of 452+ tracks, as each getQueue() call could be expensive. Consider using the skipQueue optimization here or reducing the number of retry attempts.

Suggested change
while (!queueReady && retries < 5) {
try {
const queue = await sonos.getQueue();
if (queue && queue.items && queue.items.length > 0) {
queueReady = true;
logger.debug(`Queue verified: ${queue.items.length} items ready`);
} else {
logger.debug(`Queue not ready yet (attempt ${retries + 1}/5), waiting...`);
await new Promise(resolve => setTimeout(resolve, 300));
retries++;
}
} catch (queueErr) {
logger.debug(`Queue check failed (attempt ${retries + 1}/5): ${queueErr.message}`);
await new Promise(resolve => setTimeout(resolve, 300));
retries++;
}
}
if (!queueReady) {
logger.warn('Queue not ready after 5 attempts, attempting playback anyway');
const maxQueueVerificationAttempts = 3;
while (!queueReady && retries < maxQueueVerificationAttempts) {
try {
const queue = await sonos.getQueue();
if (queue && queue.items && queue.items.length > 0) {
queueReady = true;
logger.debug(`Queue verified: ${queue.items.length} items ready`);
} else {
logger.debug(`Queue not ready yet (attempt ${retries + 1}/${maxQueueVerificationAttempts}), waiting...`);
await new Promise(resolve => setTimeout(resolve, 300));
retries++;
}
} catch (queueErr) {
logger.debug(`Queue check failed (attempt ${retries + 1}/${maxQueueVerificationAttempts}): ${queueErr.message}`);
await new Promise(resolve => setTimeout(resolve, 300));
retries++;
}
}
if (!queueReady) {
logger.warn(`Queue not ready after ${maxQueueVerificationAttempts} attempts, attempting playback anyway`);

Copilot uses AI. Check for mistakes.
}

// Try to activate queue by seeking to position 1 (alternative to SetAVTransportURI)
// This should activate the queue as the transport source
try {
logger.debug('Attempting to seek to queue position 1 to activate queue');
// Seek to track 1 in the queue to activate it
await sonos.avTransportService().Seek({
InstanceID: 0,
Unit: 'TRACK_NR',
Target: '1'
});
logger.debug('Successfully sought to track 1, queue should be active');
// Wait for seek to complete
await new Promise(resolve => setTimeout(resolve, 500));
} catch (seekErr) {
// If seek fails, try alternative: use next() to jump to first track
logger.debug('Seek failed, trying next() to activate queue: ' + seekErr.message);
try {
// Jump to first track in queue (this should activate the queue)
await sonos.next();
logger.debug('Used next() to activate queue');
await new Promise(resolve => setTimeout(resolve, 300));
} catch (nextErr) {
logger.debug('next() also failed: ' + nextErr.message);
Comment on lines +4602 to +4610

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback mechanism tries sonos.next() if seek fails, but if the queue has only 1 item (which is common when adding the first track to a stopped player), calling next() may skip past the only track or cause an error. Consider checking queue length before attempting next() or handling the case where there's only one track.

Suggested change
// If seek fails, try alternative: use next() to jump to first track
logger.debug('Seek failed, trying next() to activate queue: ' + seekErr.message);
try {
// Jump to first track in queue (this should activate the queue)
await sonos.next();
logger.debug('Used next() to activate queue');
await new Promise(resolve => setTimeout(resolve, 300));
} catch (nextErr) {
logger.debug('next() also failed: ' + nextErr.message);
// If seek fails, try alternative: carefully use next() to activate queue
logger.debug('Seek failed, considering next() to activate queue: ' + seekErr.message);
try {
// Check queue length before attempting next() to avoid skipping the only track
const queue = await sonos.getQueue();
const itemCount = queue && queue.items ? queue.items.length : 0;
if (itemCount <= 1) {
logger.debug(`Skipping next() fallback: queue has ${itemCount} item(s), relying on play()`);
} else {
// Jump to next track in queue (this should activate the queue)
await sonos.next();
logger.debug('Used next() to activate queue');
await new Promise(resolve => setTimeout(resolve, 300));
}
} catch (nextErr) {
logger.debug('Queue check or next() fallback failed: ' + nextErr.message);

Copilot uses AI. Check for mistakes.
// Continue anyway - play() might still work
}
}

// Wait a moment to ensure queue is ready
await new Promise(resolve => setTimeout(resolve, 500));

// Start playback from queue
await sonos.play();
logger.info('Started playback from queue');
Comment on lines 4552 to 4620

Copilot AI Jan 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple sleep operations create excessive delays when adding tracks to a stopped player. The function includes:

  • 500ms wait after initial stop (line 4557)
  • Up to 5 retries with 300ms waits each (lines 4566-4582, potential 1500ms)
  • 500ms wait after seek/next (lines 4600, 4608)
  • Additional 500ms wait before play (line 4616)

This could result in up to 3 seconds of delay before playback starts, which degrades user experience. Consider reducing the cumulative wait times or using exponential backoff with lower initial delays.

Copilot uses AI. Check for mistakes.
} catch (playErr) {
Expand Down
4 changes: 4 additions & 0 deletions lib/slack-validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,7 @@ module.exports = {







4 changes: 4 additions & 0 deletions lib/sonos-discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,7 @@ module.exports = {







4 changes: 4 additions & 0 deletions lib/spotify-validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,7 @@ module.exports = {







Loading