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:
Variet Worker
2026-04-16 04:58:05 +09:00
parent 66233bd9cb
commit 7ade31e4cf
6 changed files with 135 additions and 29 deletions

View File

@@ -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)) {