fix(bridge): v16 terminal output filter + v15 stale LS auto-fix + heartbeat probe (v0.5.50) #task-619
- http-bridge v16: Block terminal OUTPUT as enriched cmd — if description has no prompt marker (> » $ #), it's stdout from code block, not an actual command. Prevents 'No extension.log found' etc. from reaching Discord. - step-probe v15: Stale LS auto-detection — if all sessions are >5min old, periodically retry fixLSConnection(). Heartbeat probe every 10 polls to detect step changes when summary API returns frozen stepCount. - extension.ts v15: fixLSConnection() fallback — match LS processes without --workspace_id (common after AG restart)
This commit is contained in:
@@ -259,32 +259,40 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
||||
// ── v12: Command enrichment FIRST — extract actual command from description ──
|
||||
// Must run before filters so "Always run" with useful description isn't filtered out
|
||||
const rawCmd = (data.command || '').trim();
|
||||
const rawDesc = (data.description || '').trim();
|
||||
// v15: Strip Material icon names from description BEFORE enrichment
|
||||
// DOM textContent concatenates icon text (e.g. "content_copy") without separators
|
||||
const ICON_STRIP_RE = /\b(chevron_right|chevron_left|arrow_drop_down|arrow_drop_up|arrow_right|arrow_left|arrow_forward|arrow_back|expand_more|expand_less|more_horiz|more_vert|content_copy|content_paste|check_circle|check|keyboard_arrow_up|keyboard_arrow_down|keyboard_arrow_left|keyboard_arrow_right|slow_motion_video|open_in_new|alternate_email)\b/g;
|
||||
const rawDesc = (data.description || '').replace(ICON_STRIP_RE, '').replace(/\s{2,}/g, ' ').trim();
|
||||
const GENERIC_BTN_RE = /^(?:Always\s*)?(?:Run|Allow|Accept|Approve)$/i;
|
||||
let enrichedCmd = rawCmd;
|
||||
let enrichedDesc = rawDesc;
|
||||
if (GENERIC_BTN_RE.test(rawCmd) && rawDesc.length > 10 && rawDesc !== rawCmd) {
|
||||
// Extract the actual command from description (often includes terminal prompt)
|
||||
// Pattern: "…\project_name > actual_command"
|
||||
let extracted = rawDesc;
|
||||
const promptMatch = rawDesc.match(/[>»]\s*(.+)/);
|
||||
if (promptMatch && promptMatch[1].trim().length > 3) {
|
||||
extracted = promptMatch[1].trim();
|
||||
}
|
||||
// v12: Validate extracted text is not just a prompt fragment
|
||||
// Skip enrichment only if it looks like a bare prompt (e.g. "\\gravity_control >")
|
||||
const PROMPT_ONLY_RE = /^.*[>»$#]\s*$/;
|
||||
if (!PROMPT_ONLY_RE.test(extracted) && extracted.length > 3) {
|
||||
enrichedCmd = extracted.substring(0, 200);
|
||||
enrichedDesc = `[${rawCmd}] ${rawDesc}`;
|
||||
ctx.logToFile(`[HTTP] command enriched: "${rawCmd}" → "${enrichedCmd.substring(0, 60)}"`);
|
||||
const extracted = promptMatch[1].trim();
|
||||
// v16: Validate extracted text is not just a prompt fragment or path
|
||||
const PROMPT_ONLY_RE = /^.*[>»$#]\s*$/;
|
||||
const TERMINAL_PROMPT_RE = /^[^\n]*\\[^\\>]+\s*[>»]\s*$/;
|
||||
if (!PROMPT_ONLY_RE.test(extracted) && !TERMINAL_PROMPT_RE.test(extracted)) {
|
||||
enrichedCmd = extracted.substring(0, 200);
|
||||
enrichedDesc = `[${rawCmd}] ${rawDesc}`;
|
||||
ctx.logToFile(`[HTTP] command enriched: "${rawCmd}" → "${enrichedCmd.substring(0, 60)}"`);
|
||||
} else {
|
||||
// Prompt-only extraction — filter
|
||||
ctx.logToFile(`[HTTP] enrichment skipped (prompt-only): "${rawCmd}" desc="${rawDesc.substring(0, 60)}"`);
|
||||
ctx.logToFile(`[HTTP] filtered generic+prompt-only: "${rawCmd}"`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, filtered: true, reason: 'generic_btn_prompt_only' }));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// v13: Enrichment failed — description is prompt-only or empty
|
||||
// If cmd is still generic button text, unconditionally filter it
|
||||
ctx.logToFile(`[HTTP] enrichment skipped (prompt-only): "${rawCmd}" desc="${rawDesc.substring(0, 60)}"`);
|
||||
ctx.logToFile(`[HTTP] filtered generic+prompt-only: "${rawCmd}"`);
|
||||
// v16: No prompt marker (> » $ #) found in description — this is terminal OUTPUT, not a command
|
||||
// Observer extracted stdout text from code block (e.g. "No extension.log found", "Log found: ...")
|
||||
ctx.logToFile(`[HTTP] filtered terminal output (no prompt marker): "${rawDesc.substring(0, 60)}"`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, filtered: true, reason: 'generic_btn_prompt_only' }));
|
||||
res.end(JSON.stringify({ ok: false, filtered: true, reason: 'terminal_output' }));
|
||||
return;
|
||||
}
|
||||
} else if (GENERIC_BTN_RE.test(rawCmd) && (rawDesc.length <= 10 || rawDesc === rawCmd)) {
|
||||
@@ -307,13 +315,23 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
|
||||
// v14: Server-side junk content filter — CSS, source code, icon glue
|
||||
// This is the last line of defense regardless of observer version
|
||||
const JUNK_CONTENT_RE = /(!important|::selection|background-color:|var\(--|font-size:|border-[a-z]+:|padding:|margin:|display:\s|===|!==|\|\||\.\btest\(|\.\bmatch\(|\.\breplace\(|_RE[.\s]|\brawDesc\b|\brawCmd\b|\benrichedCmd\b|\bquerySelector\b|\.code-block|\.code-line|\.line-content|\{\s*--|integration\.build)/;
|
||||
const ICON_GLUE_RE = /(alternate_email|content_copy|content_paste|check_circle|chevron_right|chevron_left|keyboard_arrow|arrow_drop_down|arrow_drop_up|more_horiz|more_vert|expand_more|expand_less)[a-zA-Z]/;
|
||||
// v15: ICON_GLUE_RE now also catches standalone icon names (no trailing [a-zA-Z] required)
|
||||
const ICON_GLUE_RE = /\b(alternate_email|content_copy|content_paste|check_circle|chevron_right|chevron_left|keyboard_arrow|arrow_drop_down|arrow_drop_up|more_horiz|more_vert|expand_more|expand_less)\b/;
|
||||
// v15: Terminal prompt pattern — catches bare prompts like "…\project >" or "PS C:\path>"
|
||||
const BARE_PROMPT_RE = /^[^\n]{0,60}[>»$#]\s*$/;
|
||||
if (JUNK_CONTENT_RE.test(cmd) || ICON_GLUE_RE.test(cmd)) {
|
||||
ctx.logToFile(`[HTTP] filtered junk content: "${cmd.substring(0, 80)}"`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, filtered: true, reason: 'junk_content' }));
|
||||
return;
|
||||
}
|
||||
// v15: Final bare prompt filter — catches any enriched cmd that's just a terminal prompt
|
||||
if (BARE_PROMPT_RE.test(cmd) && cmd.length < 80) {
|
||||
ctx.logToFile(`[HTTP] filtered bare prompt: "${cmd.substring(0, 80)}"`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: false, filtered: true, reason: 'bare_prompt' }));
|
||||
return;
|
||||
}
|
||||
// "Run" button → step_probe handles these with full command detail
|
||||
// Only filter when step_probe IS actively tracking AND cmd is still generic button text
|
||||
if (/^(?:Always\s*)?Run\b/i.test(cmd)) {
|
||||
|
||||
Reference in New Issue
Block a user