fix(extension): Discord signal drop and UI freeze (async IO, regex filters, WS rate-limits) (v0.5.10)

This commit is contained in:
Variet Worker
2026-03-25 07:14:34 +09:00
parent 3ec45ac6b7
commit d5fdc41f35
6 changed files with 66 additions and 6 deletions

View File

@@ -189,7 +189,8 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
// ── Server-side false positive filter ──
const cmd = (data.command || '').trim();
const FALSE_POSITIVE_RE = /^(Proceed|Continue|Open|Close|OK|Yes|No|Save|Undo|Redo|Back|Next|More|Less|Got it|Deny|Allow Once|Allow This Conversation|Dismiss|Decline|Accept|Reject|Accept all|Reject all)$/i;
// Removed valid AI buttons (Accept, Reject, Allow, Deny) which are now structurally protected by the observer script
const FALSE_POSITIVE_RE = /^(Proceed|Continue|Open|Close|OK|Yes|No|Save|Undo|Redo|Back|Next|More|Less|Got it|Dismiss)$/i;
if (FALSE_POSITIVE_RE.test(cmd)) {
ctx.logToFile(`[HTTP] filtered false positive: "${cmd}"`);
res.writeHead(200, { 'Content-Type': 'application/json' });

View File

@@ -479,6 +479,12 @@ export function generateApprovalObserverScript(_port: number): string {
headers:{'Content-Type':'application/json'},
body:JSON.stringify(payload)
}).then(function(r){return r.json();}).then(function(d){
if (!d.ok || d.filtered) {
log('Pending rejected/filtered for group ['+buttonsArr2.map(function(x){return x.text;}).join(', ')+']');
delete _sent[groupKey2];
for(var di=0;di<bidList2.length;di++){delete _sent[bidList2[di]];}
return;
}
log('Pending created: '+d.request_id+' for group ['+buttonsArr2.map(function(x){return x.text;}).join(', ')+']');
pollResponseGroup(d.request_id,btnRefs2,bidList2,groupKey2);
}).catch(function(e){

View File

@@ -405,7 +405,9 @@ function setupMonitor() {
(pd.step_index === ctx.lastPendingStepIndex || (ageMs < 60_000 && ageMs >= 0));
if (isMatch) {
pd.status = 'auto_resolved';
fs.writeFileSync(pfPath, JSON.stringify(pd, null, 2), 'utf-8');
fs.promises.writeFile(pfPath, JSON.stringify(pd, null, 2), 'utf-8').catch(e => {
ctx.logToFile(`[AUTO-RESOLVE] write error: ${e.message}`);
});
resolvedCount++;
const cmd = pd.command || '';
if (cmd.length > primaryCommand.length && cmd !== 'Deny' && !cmd.includes('Allow')) {
@@ -989,7 +991,9 @@ export function writePendingApproval(data: { conversation_id: string; command: s
if (data.step_type) existing.step_type = data.step_type;
if (data.step_index !== undefined) existing.step_index = data.step_index;
existing.source = 'dom_observer+step_probe'; // mark as merged
fs.writeFileSync(efPath, JSON.stringify(existing, null, 2), 'utf-8');
fs.promises.writeFile(efPath, JSON.stringify(existing, null, 2), 'utf-8').catch(e => {
ctx.logToFile(`[DEDUP] merge write error: ${e.message}`);
});
ctx.logToFile(`[DEDUP] MERGED step_probe info into DOM pending: ${ef} cmd="${data.command.substring(0, 60)}"`);
// Record in memory dedup
if (data.step_index !== undefined && data.conversation_id) {
@@ -1071,7 +1075,9 @@ export function writePendingApproval(data: { conversation_id: string; command: s
return;
}
// File route (fallback — only when WS is NOT connected)
fs.writeFileSync(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8');
fs.promises.writeFile(path.join(pendingDir, `${id}.json`), JSON.stringify(payload, null, 2), 'utf-8').catch(e => {
console.error(`Gravity Bridge: failed to write pending: ${e.message}`);
});
console.log(`Gravity Bridge: pending approval written → ${id}.json`);
// Cache diff_review metadata in-memory (survives pending file deletion by Collector/Bot)
if (data.step_type === 'diff_review' && (data.edit_step_indices?.length || data.modified_files?.length)) {

View File

@@ -213,12 +213,21 @@ export class WSBridgeClient {
this.logFn(`[WS] Connecting to ${this.hubUrl}...`);
const ws = new WebSocket(this.hubUrl);
let connectTimeout: NodeJS.Timeout | null = null;
const clearConnectTimeout = () => {
if (connectTimeout) {
clearTimeout(connectTimeout);
connectTimeout = null;
}
};
// Detect API style: Node.js 'ws' module has .on(), browser WebSocket doesn't
const isNodeWs = typeof ws.on === 'function';
if (isNodeWs) {
// ─── Node.js ws module (EventEmitter API) ───
ws.on('open', () => {
clearConnectTimeout();
this.logFn('[WS] Connection opened, authenticating...');
this.ws = ws;
this.connected = true;
@@ -235,11 +244,18 @@ export class WSBridgeClient {
});
ws.on('close', (code: number, reason: Buffer) => {
clearConnectTimeout();
const reasonStr = reason ? reason.toString('utf-8') : '';
this.logFn(`[WS] Connection closed: code=${code} reason=${reasonStr}`);
this._onDisconnect();
});
ws.on('error', (err: any) => {
clearConnectTimeout();
this.logFn(`[WS] Connection error: ${err.message || err}`);
this._onDisconnect();
});
ws.on('pong', () => {
// Server responded to our ping — connection is alive
this.lastPongTime = Date.now();
@@ -247,6 +263,7 @@ export class WSBridgeClient {
} else {
// ─── Browser-style WebSocket API (.onopen / .onmessage) ───
ws.onopen = () => {
clearConnectTimeout();
this.logFn('[WS] Connection opened (browser API), authenticating...');
this.ws = ws;
this.connected = true;
@@ -264,15 +281,29 @@ export class WSBridgeClient {
};
ws.onclose = (event: any) => {
clearConnectTimeout();
this.logFn(`[WS] Connection closed: code=${event.code} reason=${event.reason || ''}`);
this._onDisconnect();
};
ws.onerror = (event: any) => {
clearConnectTimeout();
this.logFn(`[WS] Error: ${event.message || 'connection error'}`);
this._onDisconnect();
};
}
// Connection timeout to prevent hanging if no close/error fires
connectTimeout = setTimeout(() => {
this.logFn('[WS] Connection timeout (15s) — forcing disconnect');
if (this.ws) {
try { this.ws.terminate(); } catch { try { this.ws.close(); } catch { } }
} else if (ws) {
try { ws.terminate(); } catch { try { ws.close(); } catch { } }
}
this._onDisconnect();
}, 15000);
} catch (e: any) {
this.logFn(`[WS] Connect failed: ${e.message}`);
this._scheduleReconnect();
@@ -448,13 +479,15 @@ export class WSBridgeClient {
}
}
private _flushQueue(): void {
private async _flushQueue(): Promise<void> {
if (this.messageQueue.length === 0) return;
this.logFn(`[WS] Flushing ${this.messageQueue.length} queued messages`);
this.logFn(`[WS] Flushing ${this.messageQueue.length} queued messages (paced)`);
const queue = [...this.messageQueue];
this.messageQueue = [];
for (const msg of queue) {
this._sendRaw(msg);
// Pace the burst to avoid hitting the Hub's rate limit (60 msgs / 10s)
await new Promise(r => setTimeout(r, 50));
}
}