docs: update devlog with perf optimization entry + compiled output

This commit is contained in:
2026-03-15 10:54:24 +09:00
parent ae0509fbb5
commit 75289b3ec5
3 changed files with 61 additions and 13 deletions

View File

@@ -4,4 +4,5 @@
|---|------|------|------|------| |---|------|------|------|------|
| 001 | 07:00~08:16 | 승인 신호 누락 진단 & 5건 버그 수정 (DEDUP collision, fs.watch fail, default 보호, auto 확인, msg dedup) | `40e3cd5` | ✅ | | 001 | 07:00~08:16 | 승인 신호 누락 진단 & 5건 버그 수정 (DEDUP collision, fs.watch fail, default 보호, auto 확인, msg dedup) | `40e3cd5` | ✅ |
| 002 | 08:25~08:31 | Extension v0.3.10 버전 범프 & VSIX 빌드 | `10caae1` | ✅ | | 002 | 08:25~08:31 | Extension v0.3.10 버전 범프 & VSIX 빌드 | `10caae1` | ✅ |
| 003 | 10:00~10:41 | 승인 라이프사이클 race condition 4건 수정 (HTML lock, pending status skip, auto_resolve Discord 알림, Bot approval_messages) | `015aa79` | ✅ | | 003 | 10:00~10:41 | 승인 라이프사이클 race condition 4건 수정 (HTML lock, pending status skip, auto_resolve Discord 알림, Bot approval_messages) | `f962036` | ✅ |
| 004 | 10:41~10:53 | 성능 최적화 3건 (pollResponseGroup 1500ms, renderer adaptive idle, Bot single-pass scanner) + VSIX 빌드 | `ae0509f` | ✅ |

View File

@@ -502,6 +502,24 @@ async function setupApprovalObserver() {
requiredScript: 'jetskiAgent.js', // JS entry point requiredScript: 'jetskiAgent.js', // JS entry point
}, },
]; ];
// ── FIX #1: File lock to prevent multi-instance HTML patching race ──
const lockFile = path.join(scriptDir, '.patch-lock');
let lockAcquired = false;
try {
if (fs.existsSync(lockFile)) {
const lockAge = Date.now() - fs.statSync(lockFile).mtimeMs;
if (lockAge < 30_000) {
logToFile(`[OBSERVER] another instance is patching (lock age=${Math.round(lockAge / 1000)}s) — skipping`);
return; // Exit setupApprovalObserver entirely
}
logToFile(`[OBSERVER] stale lock (age=${Math.round(lockAge / 1000)}s) — force-acquiring`);
}
fs.writeFileSync(lockFile, JSON.stringify({ pid: process.pid, ts: Date.now() }), 'utf-8');
lockAcquired = true;
}
catch (lockErr) {
logToFile(`[OBSERVER] lock acquire error: ${lockErr.message} — proceeding anyway`);
}
for (const spec of htmlFileSpecs) { for (const spec of htmlFileSpecs) {
const htmlPath = path.join(scriptDir, spec.name); const htmlPath = path.join(scriptDir, spec.name);
const backupPath = htmlPath + '.orig'; const backupPath = htmlPath + '.orig';
@@ -592,6 +610,14 @@ async function setupApprovalObserver() {
logToFile(`[OBSERVER] ${spec.name} patch error: ${e.message}`); logToFile(`[OBSERVER] ${spec.name} patch error: ${e.message}`);
} }
} }
// Release patch lock
if (lockAcquired) {
try {
fs.unlinkSync(lockFile);
}
catch { }
logToFile('[OBSERVER] patch lock released');
}
} }
// 4. Update product.json checksums so vscode-file:// serves our patched files // 4. Update product.json checksums so vscode-file:// serves our patched files
updateProductChecksums(); updateProductChecksums();
@@ -1409,7 +1435,7 @@ function generateApprovalObserverScript(_port) {
// ── Poll for Discord response (multi-button group aware) ── // ── Poll for Discord response (multi-button group aware) ──
function pollResponseGroup(rid,btnRefs,bidList,groupKey){ function pollResponseGroup(rid,btnRefs,bidList,groupKey){
var polls=0; var polls=0;
var maxPolls=600; // 5 minutes at 500ms interval var maxPolls=200; // 5 minutes at 1500ms interval
var timer=setInterval(function(){ var timer=setInterval(function(){
polls++; polls++;
// Check if ANY button in the group is still in DOM // Check if ANY button in the group is still in DOM
@@ -1454,7 +1480,7 @@ function generateApprovalObserverScript(_port) {
delete _sent[groupKey]; delete _sent[groupKey];
for(var ri=0;ri<bidList.length;ri++){delete _sent[bidList[ri]];} for(var ri=0;ri<bidList.length;ri++){delete _sent[bidList[ri]];}
}).catch(function(){}); }).catch(function(){});
},500); },1500);
} }
// Legacy pollResponse for backward compatibility (single button) // Legacy pollResponse for backward compatibility (single button)
@@ -1532,19 +1558,27 @@ function generateApprovalObserverScript(_port) {
// FALLBACK: periodic scan every 3s for any missed mutations // FALLBACK: periodic scan every 3s for any missed mutations
setInterval(scheduleScan,3000); setInterval(scheduleScan,3000);
// ── Adaptive idle detection for HTTP polls ──
var _lastActivity=Date.now();
var _idleThreshold=60000; // 60s without DOM changes → slow mode
new MutationObserver(function(){_lastActivity=Date.now();}).observe(document.body,{childList:true,subtree:true,attributes:true});
function getAdaptiveInterval(){return (Date.now()-_lastActivity>_idleThreshold)?10000:2000;}
// ── DEEP-INSPECT POLLING: curl→Bridge→Renderer→Results ── // ── DEEP-INSPECT POLLING: curl→Bridge→Renderer→Results ──
setInterval(function(){ (function pollDeepInspect(){
if(!_ready||!BASE)return; if(_ready&&BASE){
fetch(BASE+'/deep-inspect-trigger?t='+Date.now()).then(function(r){return r.json();}).then(function(d){ fetch(BASE+'/deep-inspect-trigger?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
if(d.inspect){log('🔍 Deep inspect triggered via HTTP');runDeepInspect();} if(d.inspect){log('🔍 Deep inspect triggered via HTTP');runDeepInspect();}
}).catch(function(){}); }).catch(function(){});
},2000); }
setTimeout(pollDeepInspect,getAdaptiveInterval());
})();
// ── TRIGGER-CLICK: Extension→Renderer bridge for programmatic button clicks ── // ── TRIGGER-CLICK: Extension→Renderer bridge for programmatic button clicks ──
// Extension sets clickTrigger via tryApprovalStrategies → renderer polls and clicks // Extension sets clickTrigger via tryApprovalStrategies → renderer polls and clicks
// v3: uses deepFindButtons() to traverse iframes, webviews, shadow DOMs // v3: uses deepFindButtons() to traverse iframes, webviews, shadow DOMs
setInterval(function(){ (function pollTriggerClick(){
if(!_ready||!BASE)return; if(_ready&&BASE){
fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){ fetch(BASE+'/trigger-click?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
if(!d.action)return; if(!d.action)return;
log('🔔 TRIGGER-CLICK received: action='+d.action); log('🔔 TRIGGER-CLICK received: action='+d.action);
@@ -1632,7 +1666,9 @@ function generateApprovalObserverScript(_port) {
log('⚠️ iframes='+document.querySelectorAll('iframe').length+' webviews='+document.querySelectorAll('webview').length); log('⚠️ iframes='+document.querySelectorAll('iframe').length+' webviews='+document.querySelectorAll('webview').length);
} }
}).catch(function(){}); }).catch(function(){});
},1000); }
setTimeout(pollTriggerClick,getAdaptiveInterval());
})();
_obs=true; _obs=true;
log('v3 Observer active — deep DOM traversal + MutationObserver + trigger-click polling'); log('v3 Observer active — deep DOM traversal + MutationObserver + trigger-click polling');
@@ -1916,6 +1952,8 @@ function setupMonitor() {
pd.status = 'auto_resolved'; pd.status = 'auto_resolved';
fs.writeFileSync(pfPath, JSON.stringify(pd, null, 2), 'utf-8'); fs.writeFileSync(pfPath, JSON.stringify(pd, null, 2), 'utf-8');
logToFile(`[AUTO-RESOLVE] step=${lastPendingStepIndex} progressed → marked ${pf} (age=${Math.round(ageMs / 1000)}s)`); logToFile(`[AUTO-RESOLVE] step=${lastPendingStepIndex} progressed → marked ${pf} (age=${Math.round(ageMs / 1000)}s)`);
// FIX #3: Notify Discord that user approved locally
writeChatSnapshot(`✅ **AG에서 직접 승인됨** (step ${lastPendingStepIndex})\n\n\`${(pd.command || '').substring(0, 200)}\``);
} }
} }
} }
@@ -2482,6 +2520,15 @@ async function processResponseFile(filePath) {
if (fs.existsSync(pendingFile)) { if (fs.existsSync(pendingFile)) {
try { try {
const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8')); const pending = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
// FIX #2: Skip if pending was already resolved locally (auto_resolve or expired)
if (pending.status === 'auto_resolved' || pending.status === 'expired') {
logToFile(`[RESPONSE] SKIP — pending already ${pending.status} (rid=${resp.request_id})`);
try {
fs.unlinkSync(filePath);
}
catch { }
return;
}
sessionId = pending.conversation_id || ''; sessionId = pending.conversation_id || '';
isDomObserver = pending.auto_detected === true isDomObserver = pending.auto_detected === true
|| pending.source === 'dom_observer'; || pending.source === 'dom_observer';

File diff suppressed because one or more lines are too long