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
53 changes: 48 additions & 5 deletions src/chrome/src/agent/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,7 @@ export class Agent extends LoopDetector {
// answer. Track long observation-only streaks and remind it to deliver a
// useful result before exhausting the run.
this.deliveryObservationStreaks = new Map(); // tabId -> count
this.deliveryActionableDiscoveryResets = new Set(); // tabIds that used their one discovery reset since meaningful progress
this.lastAutoScreenshotTs = new Map(); // tabId -> ms — defensive debounce
this.lastSeenAdapter = new Map(); // tabId -> adapter name from last enrichment
// Per-tab opt-in: when true, the agent is allowed to use API mutations
Expand Down Expand Up @@ -2555,6 +2556,7 @@ export class Agent extends LoopDetector {
this._uploadSelectorRecoveryRequired.delete(tabId);
this._compactUploadTargets.delete(tabId);
this.deliveryObservationStreaks.delete(tabId);
this.deliveryActionableDiscoveryResets.delete(tabId);
this.bulkApiMutationClicks.delete(tabId);
this.bulkApiMutationHints.delete(tabId);
const replayFailurePrefix = `${tabId}|`;
Expand Down Expand Up @@ -3247,6 +3249,17 @@ export class Agent extends LoopDetector {
_checkDeliveryObservationStreak(tabId, name, args = {}, result = null, options = {}) {
const observation = this.constructor.DELIVERY_OBSERVATION_TOOLS.has(name)
&& !isNetworkMutation(name, args);
if (observation
&& options.discoveredActionableTargets === true
&& !this.deliveryActionableDiscoveryResets.has(tabId)) {
// Give structured target discovery one free observation per verified
// progress interval. Paginating through newly discovered controls cannot
// repeatedly erase the delivery guard; meaningful consequential progress
// below rearms the one-shot reset.
this.deliveryActionableDiscoveryResets.add(tabId);
this.deliveryObservationStreaks.delete(tabId);
return { kind: 'none' };
}
if (observation && options.requiredReadProgress === true) {
// A new page in the runtime-required complete-thread scope is bounded,
// deterministic progress, not aimless research drift. Let exact trusted
Expand All @@ -3267,6 +3280,7 @@ export class Agent extends LoopDetector {
// verified consequential progress or a real progress-ledger mutation.
if (this._deliveryCheckpointMadeMeaningfulProgress(name, result, options)) {
this.deliveryObservationStreaks.delete(tabId);
this.deliveryActionableDiscoveryResets.delete(tabId);
}
return { kind: 'none' };
}
Expand Down Expand Up @@ -7329,6 +7343,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
completionStateBeforeTool,
completionStateAfterTool,
),
discoveredActionableTargets: Number(progressObserved?.addedPending || 0) > 0,
requiredReadProgress,
// Ask research can lose a useful deliverable to the same observation
// drift as Act/Dev. Any interactive mode that advertises `done`
Expand Down Expand Up @@ -7435,7 +7450,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
});
}
if (progressObserved) {
resultContent += `\n[PROGRESS LEDGER OBSERVED: GitHub stargazers buttons observed=${progressObserved.observedButtons}; added ${progressObserved.addedPending} pending Follow row(s); skipped ${progressObserved.alreadyFollowedSkipped} already-followed row(s) and ${progressObserved.excludedSkipped} excluded row(s). Only rows created from visible Follow buttons need follow action.]`;
resultContent += `\n[PROGRESS LEDGER OBSERVED: GitHub follow buttons observed=${progressObserved.observedButtons}; added ${progressObserved.addedPending} pending Follow row(s); skipped ${progressObserved.alreadyFollowedSkipped} already-followed row(s) and ${progressObserved.excludedSkipped} excluded row(s). Only rows created from visible Follow buttons need follow action.]`;
}
if (progressAuto) {
resultContent += '\n' + this._progressAutoRecordedNote(progressAuto.item);
Expand Down Expand Up @@ -12426,6 +12441,18 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
};
}

_deterministicDeliveryProgressPartial(tabId) {
const rows = this._currentTaskLedgerRows(tabId);
if (!rows.length) return '';
const counts = progressCounts(rows);
const summary = [
'Browser observation limit reached before the full task scope could be verified.',
`Partial progress was preserved from the app-owned ledger: ${counts.total} recorded item(s) — ${counts.processed} processed, ${counts.skipped} skipped, ${counts.failed} failed, ${counts.pending} pending, and ${counts.acted} acted but not fully resolved.`,
'No further browser observations or actions were performed after the cutoff.',
].join(' ');
return this._appendProgressLedgerToFinal(tabId, summary);
}

async _recoverDeliveryCheckpointTurn(
tabId,
messages,
Expand Down Expand Up @@ -12469,13 +12496,16 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
const stopped = this._consumeContextOnlyAbort(tabId, messages, onUpdate);
if (stopped) return stopped;
if (!recovered) {
const content = fallbackMessage || (protectedPageRecovery
const deterministicPartial = protectedPageRecovery
? ''
: this._deterministicDeliveryProgressPartial(tabId);
const content = deterministicPartial || fallbackMessage || (protectedPageRecovery
? 'Chrome protected this Chrome Web Store page, and WebBrain could not produce a useful answer from the one visual fallback. Leave the page open and continue manually.'
: 'I gathered information but could not produce a valid partial result after reaching the browser observation limit.');
const status = preservedStatus || 'delivery_recovery_failed';
const status = preservedStatus || (deterministicPartial ? 'partial' : 'delivery_recovery_failed');
messages.push({ role: 'assistant', content });
onUpdate('text', { content, replace: true });
onUpdate('error', { message: content });
onUpdate(deterministicPartial ? 'warning' : 'error', { message: content });
onUpdate('run_status', { status, message: content });
this._persist(tabId);
return { content, status };
Expand Down Expand Up @@ -16262,6 +16292,19 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
}
}

_isGithubFollowListUrl(url) {
if (this._isGithubStargazersUrl(url)) return true;
try {
const parsed = new URL(url);
if (parsed.hostname !== 'github.com') return false;
const parts = parsed.pathname.split('/').filter(Boolean);
if (parts.length >= 3 && parts[0] === 'orgs' && parts[2] === 'followers') return true;
return parts.length === 1 && ['followers', 'following'].includes(parsed.searchParams.get('tab') || '');
} catch {
return false;
}
}

_mastodonPageContentFromResult(result = {}) {
if (!result || typeof result !== 'object') return '';
const candidates = [
Expand Down Expand Up @@ -16311,7 +16354,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
const pageContent = result.pageContent || result.text || '';
if (!pageContent || (!pageContent.includes('button "Follow ') && !pageContent.includes('button "Unfollow '))) return null;
const url = result.url || result.pageUrl || await this._currentUrl(tabId);
if (!this._isGithubStargazersUrl(url)) return null;
if (!this._isGithubFollowListUrl(url)) return null;
const pageScope = this._rememberProgressPageScope(tabId, url);
const session = this._progressSessionForObservation(tabId, { pageScope });
if (!isProgressActionAllowed(session, 'follow')) return null;
Expand Down
54 changes: 48 additions & 6 deletions src/firefox/src/agent/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ export class Agent extends LoopDetector {
// answer. Track long observation-only streaks and remind it to deliver a
// useful result before exhausting the run.
this.deliveryObservationStreaks = new Map();
this.deliveryActionableDiscoveryResets = new Set();
// Local screenshot redaction (issue #312). When true, screenshots sent
// to a Vision endpoint are pixelated over DOM-detected PII regions
// (form fields + email/phone text) BEFORE leaving the extension. Off by
Expand Down Expand Up @@ -2340,6 +2341,7 @@ export class Agent extends LoopDetector {
this._uploadSelectorRecoveryRequired.delete(tabId);
this._compactUploadTargets.delete(tabId);
this.deliveryObservationStreaks.delete(tabId);
this.deliveryActionableDiscoveryResets.delete(tabId);
this.bulkApiMutationClicks.delete(tabId);
this.bulkApiMutationHints.delete(tabId);
const replayFailurePrefix = `${tabId}|`;
Expand Down Expand Up @@ -3019,6 +3021,17 @@ export class Agent extends LoopDetector {
_checkDeliveryObservationStreak(tabId, name, args = {}, result = null, options = {}) {
const observation = this.constructor.DELIVERY_OBSERVATION_TOOLS.has(name)
&& !isNetworkMutation(name, args);
if (observation
&& options.discoveredActionableTargets === true
&& !this.deliveryActionableDiscoveryResets.has(tabId)) {
// Give structured target discovery one free observation per verified
// progress interval. Paginating through newly discovered controls cannot
// repeatedly erase the delivery guard; meaningful consequential progress
// below rearms the one-shot reset.
this.deliveryActionableDiscoveryResets.add(tabId);
this.deliveryObservationStreaks.delete(tabId);
return { kind: 'none' };
}
if (observation && options.requiredReadProgress === true) {
// A new page in the runtime-required complete-thread scope is bounded,
// deterministic progress, not aimless research drift. Let exact trusted
Expand All @@ -3039,6 +3052,7 @@ export class Agent extends LoopDetector {
// verified consequential progress or a real progress-ledger mutation.
if (this._deliveryCheckpointMadeMeaningfulProgress(name, result, options)) {
this.deliveryObservationStreaks.delete(tabId);
this.deliveryActionableDiscoveryResets.delete(tabId);
}
return { kind: 'none' };
}
Expand Down Expand Up @@ -5897,6 +5911,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
completionStateBeforeTool,
completionStateAfterTool,
),
discoveredActionableTargets: Number(progressObserved?.addedPending || 0) > 0,
requiredReadProgress,
// Ask research can lose a useful deliverable to the same observation
// drift as Act/Dev. Any interactive mode that advertises `done`
Expand Down Expand Up @@ -5987,7 +6002,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
});
}
if (progressObserved) {
resultContent += `\n[PROGRESS LEDGER OBSERVED: GitHub stargazers buttons observed=${progressObserved.observedButtons}; added ${progressObserved.addedPending} pending Follow row(s); skipped ${progressObserved.alreadyFollowedSkipped} already-followed row(s) and ${progressObserved.excludedSkipped} excluded row(s). Only rows created from visible Follow buttons need follow action.]`;
resultContent += `\n[PROGRESS LEDGER OBSERVED: GitHub follow buttons observed=${progressObserved.observedButtons}; added ${progressObserved.addedPending} pending Follow row(s); skipped ${progressObserved.alreadyFollowedSkipped} already-followed row(s) and ${progressObserved.excludedSkipped} excluded row(s). Only rows created from visible Follow buttons need follow action.]`;
}
if (progressAuto) {
resultContent += '\n' + this._progressAutoRecordedNote(progressAuto.item);
Expand Down Expand Up @@ -10336,6 +10351,18 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
};
}

_deterministicDeliveryProgressPartial(tabId) {
const rows = this._currentTaskLedgerRows(tabId);
if (!rows.length) return '';
const counts = progressCounts(rows);
const summary = [
'Browser observation limit reached before the full task scope could be verified.',
`Partial progress was preserved from the app-owned ledger: ${counts.total} recorded item(s) — ${counts.processed} processed, ${counts.skipped} skipped, ${counts.failed} failed, ${counts.pending} pending, and ${counts.acted} acted but not fully resolved.`,
'No further browser observations or actions were performed after the cutoff.',
].join(' ');
return this._appendProgressLedgerToFinal(tabId, summary);
}

async _recoverDeliveryCheckpointTurn(
tabId,
messages,
Expand Down Expand Up @@ -10368,13 +10395,15 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
const stopped = this._consumeContextOnlyAbort(tabId, messages, onUpdate);
if (stopped) return stopped;
if (!recovered) {
const content = fallbackMessage || 'I gathered information but could not produce a valid partial result after reaching the browser observation limit.';
const deterministicPartial = this._deterministicDeliveryProgressPartial(tabId);
const content = deterministicPartial || fallbackMessage || 'I gathered information but could not produce a valid partial result after reaching the browser observation limit.';
const status = deterministicPartial ? 'partial' : 'delivery_recovery_failed';
messages.push({ role: 'assistant', content });
onUpdate('text', { content, replace: true });
onUpdate('error', { message: content });
onUpdate('run_status', { status: 'delivery_recovery_failed', message: content });
onUpdate(deterministicPartial ? 'warning' : 'error', { message: content });
onUpdate('run_status', { status, message: content });
this._persist(tabId);
return { content, status: 'delivery_recovery_failed' };
return { content, status };
}
const toolResult = {
done: true,
Expand Down Expand Up @@ -14010,6 +14039,19 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
}
}

_isGithubFollowListUrl(url) {
if (this._isGithubStargazersUrl(url)) return true;
try {
const parsed = new URL(url);
if (parsed.hostname !== 'github.com') return false;
const parts = parsed.pathname.split('/').filter(Boolean);
if (parts.length >= 3 && parts[0] === 'orgs' && parts[2] === 'followers') return true;
return parts.length === 1 && ['followers', 'following'].includes(parsed.searchParams.get('tab') || '');
} catch {
return false;
}
}

_mastodonPageContentFromResult(result = {}) {
if (!result || typeof result !== 'object') return '';
const candidates = [
Expand Down Expand Up @@ -14059,7 +14101,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
const pageContent = result.pageContent || result.text || '';
if (!pageContent || (!pageContent.includes('button "Follow ') && !pageContent.includes('button "Unfollow '))) return null;
const url = result.url || result.pageUrl || await this._currentUrl(tabId);
if (!this._isGithubStargazersUrl(url)) return null;
if (!this._isGithubFollowListUrl(url)) return null;
const pageScope = this._rememberProgressPageScope(tabId, url);
const session = this._progressSessionForObservation(tabId, { pageScope });
if (!isProgressActionAllowed(session, 'follow')) return null;
Expand Down
Loading
Loading