fix(ext): !stop getActiveSessionId stale primitive — use step-probe getter #task-410
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"displayName": "Gravity Bridge",
|
"displayName": "Gravity Bridge",
|
||||||
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
||||||
"version": "0.4.5",
|
"version": "0.4.6",
|
||||||
"publisher": "variet",
|
"publisher": "variet",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.100.0"
|
"vscode": "^1.100.0"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import * as path from 'path';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as cp from 'child_process';
|
import * as cp from 'child_process';
|
||||||
import { WSBridgeClient, WSResponseData, WSCommandData } from './ws-client';
|
import { WSBridgeClient, WSResponseData, WSCommandData } from './ws-client';
|
||||||
import { initStepProbe, BridgeContext, writePendingApproval, tryApprovalStrategies, writeRegistration, getApprovalContext, resetPendingState, handleDiffReviewResponse } from './step-probe';
|
import { initStepProbe, BridgeContext, writePendingApproval, tryApprovalStrategies, writeRegistration, getApprovalContext, resetPendingState, handleDiffReviewResponse, getActiveSessionId as getStepProbeSessionId } from './step-probe';
|
||||||
import { startHttpBridge, getDeterministicPort, HttpBridgeContext } from './http-bridge';
|
import { startHttpBridge, getDeterministicPort, HttpBridgeContext } from './http-bridge';
|
||||||
import { setupApprovalObserver } from './html-patcher';
|
import { setupApprovalObserver } from './html-patcher';
|
||||||
import { watchCommandsDir, handleWSCommand, disposeCommandsWatcher, CommandHandlerContext } from './command-handler';
|
import { watchCommandsDir, handleWSCommand, disposeCommandsWatcher, CommandHandlerContext } from './command-handler';
|
||||||
@@ -430,7 +430,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
bridgePath, projectName, sdk, ls: sdk?.ls, autoApproveEnabled, logToFile,
|
bridgePath, projectName, sdk, ls: sdk?.ls, autoApproveEnabled, logToFile,
|
||||||
onAutoApproveChanged: (enabled: boolean) => { autoApproveEnabled = enabled; },
|
onAutoApproveChanged: (enabled: boolean) => { autoApproveEnabled = enabled; },
|
||||||
recentDiscordSentTexts,
|
recentDiscordSentTexts,
|
||||||
getActiveSessionId: () => activeSessionId,
|
getActiveSessionId: () => getStepProbeSessionId(),
|
||||||
}, data);
|
}, data);
|
||||||
},
|
},
|
||||||
onInstanceUpdate: (count, instances) => {
|
onInstanceUpdate: (count, instances) => {
|
||||||
@@ -560,7 +560,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
bridgePath, projectName, sdk, ls: sdk?.ls, autoApproveEnabled, logToFile,
|
bridgePath, projectName, sdk, ls: sdk?.ls, autoApproveEnabled, logToFile,
|
||||||
onAutoApproveChanged: (enabled: boolean) => { autoApproveEnabled = enabled; },
|
onAutoApproveChanged: (enabled: boolean) => { autoApproveEnabled = enabled; },
|
||||||
recentDiscordSentTexts,
|
recentDiscordSentTexts,
|
||||||
getActiveSessionId: () => activeSessionId,
|
getActiveSessionId: () => getStepProbeSessionId(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Response watcher is now initialized by initStepProbe() above
|
// Response watcher is now initialized by initStepProbe() above
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export interface BridgeContext {
|
|||||||
diffReviewMetadata: Map<string, { edit_step_indices: number[]; modified_files: string[] }>;
|
diffReviewMetadata: Map<string, { edit_step_indices: number[]; modified_files: string[] }>;
|
||||||
recentDiscordSentTexts: Map<string, number>;
|
recentDiscordSentTexts: Map<string, number>;
|
||||||
writeChatSnapshot: (text: string) => void;
|
writeChatSnapshot: (text: string) => void;
|
||||||
writeChatSnapshotWithFiles: (text: string, files: Array<{name: string, content: string}>) => void;
|
writeChatSnapshotWithFiles: (text: string, files: Array<{ name: string, content: string }>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ctx: BridgeContext;
|
let ctx: BridgeContext;
|
||||||
@@ -52,6 +52,18 @@ export function getApprovalContext(): { sessionId: string; stepIndex: number } {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active session ID tracked by step-probe polling.
|
||||||
|
* Used by command-handler (via extension.ts) for !stop → CancelCascadeInvocation.
|
||||||
|
*
|
||||||
|
* CRITICAL: extension.ts must use this getter instead of its own module-level
|
||||||
|
* `activeSessionId` variable, which is a stale primitive copy that never updates
|
||||||
|
* when step-probe discovers new sessions.
|
||||||
|
*/
|
||||||
|
export function getActiveSessionId(): string {
|
||||||
|
return ctx?.activeSessionId || '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset pending state after successful approval.
|
* Reset pending state after successful approval.
|
||||||
* Called after WS response triggers approval in extension.ts.
|
* Called after WS response triggers approval in extension.ts.
|
||||||
@@ -220,9 +232,9 @@ function setupMonitor() {
|
|||||||
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
|
let lastRelayedTaskText = ''; // dedup TASK_BOUNDARY relay
|
||||||
let wasRunning = false; // track RUNNING→IDLE transition for response capture
|
let wasRunning = false; // track RUNNING→IDLE transition for response capture
|
||||||
let lastUserInputStepIdx = -1; // track user input for response matching
|
let lastUserInputStepIdx = -1; // track user input for response matching
|
||||||
let pendingModifiedFiles: string[] = []; // accumulate modified files during RUNNING
|
let pendingModifiedFiles: string[] = []; // accumulate modified files during RUNNING
|
||||||
let pendingModifiedFilePaths: string[] = []; // full paths for diff review
|
let pendingModifiedFilePaths: string[] = []; // full paths for diff review
|
||||||
let pendingEditStepIndices: number[] = []; // step indices for AcknowledgeCascadeCodeEdit
|
let pendingEditStepIndices: number[] = []; // step indices for AcknowledgeCascadeCodeEdit
|
||||||
let lastResponseCaptureStep = -1; // dedup: don't capture same response twice
|
let lastResponseCaptureStep = -1; // dedup: don't capture same response twice
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
@@ -360,22 +372,22 @@ function setupMonitor() {
|
|||||||
const actualIdx = rtOffset + ri;
|
const actualIdx = rtOffset + ri;
|
||||||
if (actualIdx <= lastResponseCaptureStep) continue;
|
if (actualIdx <= lastResponseCaptureStep) continue;
|
||||||
// Track file write steps for diff review
|
// Track file write steps for diff review
|
||||||
if (s?.metadata?.toolCall?.argumentsJson) {
|
if (s?.metadata?.toolCall?.argumentsJson) {
|
||||||
try {
|
try {
|
||||||
const tcArgs = JSON.parse(s.metadata.toolCall.argumentsJson);
|
const tcArgs = JSON.parse(s.metadata.toolCall.argumentsJson);
|
||||||
const tf = tcArgs.TargetFile || tcArgs.target_file || '';
|
const tf = tcArgs.TargetFile || tcArgs.target_file || '';
|
||||||
if (tf) {
|
if (tf) {
|
||||||
const bn = tf.split(/[\\/]/).pop() || tf;
|
const bn = tf.split(/[\\/]/).pop() || tf;
|
||||||
if (!pendingModifiedFiles.includes(bn)) {
|
if (!pendingModifiedFiles.includes(bn)) {
|
||||||
pendingModifiedFiles.push(bn);
|
pendingModifiedFiles.push(bn);
|
||||||
pendingModifiedFilePaths.push(tf);
|
pendingModifiedFilePaths.push(tf);
|
||||||
pendingEditStepIndices.push(actualIdx);
|
pendingEditStepIndices.push(actualIdx);
|
||||||
ctx.logToFile(`[DIFF-TRACK] + ${bn} (step ${actualIdx})`);
|
ctx.logToFile(`[DIFF-TRACK] + ${bn} (step ${actualIdx})`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} catch { }
|
||||||
} catch { }
|
}
|
||||||
}
|
if (sType.includes('PLANNER_RESPONSE') && s?.status?.includes('DONE')) {
|
||||||
if (sType.includes('PLANNER_RESPONSE') && s?.status?.includes('DONE')) {
|
|
||||||
const pr = s?.plannerResponse;
|
const pr = s?.plannerResponse;
|
||||||
if (pr) {
|
if (pr) {
|
||||||
let text = pr.modifiedResponse || pr.rawText || pr.text || '';
|
let text = pr.modifiedResponse || pr.rawText || pr.text || '';
|
||||||
@@ -425,37 +437,37 @@ function setupMonitor() {
|
|||||||
// Steps progressed — if we had a pending approval, it was handled in AG directly
|
// Steps progressed — if we had a pending approval, it was handled in AG directly
|
||||||
if (!ctx.sawRunningAfterPending && ctx.lastPendingStepIndex >= 0) {
|
if (!ctx.sawRunningAfterPending && ctx.lastPendingStepIndex >= 0) {
|
||||||
// Mark pending as auto_resolved so bot can update Discord message
|
// Mark pending as auto_resolved so bot can update Discord message
|
||||||
let resolvedCount = 0;
|
let resolvedCount = 0;
|
||||||
let primaryCommand = '';
|
let primaryCommand = '';
|
||||||
const pendingFiles = fs.readdirSync(path.join(ctx.bridgePath, 'pending')).filter((f: string) => f.endsWith('.json'));
|
const pendingFiles = fs.readdirSync(path.join(ctx.bridgePath, 'pending')).filter((f: string) => f.endsWith('.json'));
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
for (const pf of pendingFiles) {
|
for (const pf of pendingFiles) {
|
||||||
const pfPath = path.join(ctx.bridgePath, 'pending', pf);
|
const pfPath = path.join(ctx.bridgePath, 'pending', pf);
|
||||||
try {
|
try {
|
||||||
const pd = JSON.parse(fs.readFileSync(pfPath, 'utf-8'));
|
const pd = JSON.parse(fs.readFileSync(pfPath, 'utf-8'));
|
||||||
if (pd.status !== 'pending') continue;
|
if (pd.status !== 'pending') continue;
|
||||||
if (pd.project_name && pd.project_name !== ctx.projectName) continue;
|
if (pd.project_name && pd.project_name !== ctx.projectName) continue;
|
||||||
// Limit to same session AND (same step or recent)
|
// Limit to same session AND (same step or recent)
|
||||||
const ageMs = nowMs - (pd.timestamp * 1000);
|
const ageMs = nowMs - (pd.timestamp * 1000);
|
||||||
const isMatch = (pd.conversation_id === ctx.activeSessionId) &&
|
const isMatch = (pd.conversation_id === ctx.activeSessionId) &&
|
||||||
(pd.step_index === ctx.lastPendingStepIndex || (ageMs < 60_000 && ageMs >= 0));
|
(pd.step_index === ctx.lastPendingStepIndex || (ageMs < 60_000 && ageMs >= 0));
|
||||||
if (isMatch) {
|
if (isMatch) {
|
||||||
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');
|
||||||
resolvedCount++;
|
resolvedCount++;
|
||||||
const cmd = pd.command || '';
|
const cmd = pd.command || '';
|
||||||
if (cmd.length > primaryCommand.length && cmd !== 'Deny' && !cmd.includes('Allow')) {
|
if (cmd.length > primaryCommand.length && cmd !== 'Deny' && !cmd.includes('Allow')) {
|
||||||
primaryCommand = cmd;
|
primaryCommand = cmd;
|
||||||
} else if (!primaryCommand) {
|
} else if (!primaryCommand) {
|
||||||
primaryCommand = cmd;
|
primaryCommand = cmd;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: any) { ctx.logToFile(`[AUTO-RESOLVE] parse error for ${pf}: ${e.message}`); }
|
}
|
||||||
}
|
} catch (e: any) { ctx.logToFile(`[AUTO-RESOLVE] parse error for ${pf}: ${e.message}`); }
|
||||||
if (resolvedCount > 0) {
|
}
|
||||||
ctx.logToFile(`[AUTO-RESOLVE] step=${ctx.lastPendingStepIndex} progressed → marked ${resolvedCount} pending(s)`);
|
if (resolvedCount > 0) {
|
||||||
ctx.writeChatSnapshot(`✅ **AG에서 직접 진행됨** (step ${ctx.lastPendingStepIndex})\n\n\`${primaryCommand.substring(0, 200)}\``);
|
ctx.logToFile(`[AUTO-RESOLVE] step=${ctx.lastPendingStepIndex} progressed → marked ${resolvedCount} pending(s)`);
|
||||||
}
|
ctx.writeChatSnapshot(`✅ **AG에서 직접 진행됨** (step ${ctx.lastPendingStepIndex})\n\n\`${primaryCommand.substring(0, 200)}\``);
|
||||||
|
}
|
||||||
ctx.lastPendingStepIndex = -1;
|
ctx.lastPendingStepIndex = -1;
|
||||||
// Clear memory dedup for this session (step progressed, new WAITING steps are allowed)
|
// Clear memory dedup for this session (step progressed, new WAITING steps are allowed)
|
||||||
for (const k of recentPendingSteps.keys()) {
|
for (const k of recentPendingSteps.keys()) {
|
||||||
@@ -548,7 +560,7 @@ function setupMonitor() {
|
|||||||
description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`,
|
description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`,
|
||||||
step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission'
|
step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission'
|
||||||
: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit'
|
: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit'
|
||||||
: toolName,
|
: toolName,
|
||||||
step_index: actualIndex,
|
step_index: actualIndex,
|
||||||
source: 'step_probe_offset',
|
source: 'step_probe_offset',
|
||||||
});
|
});
|
||||||
@@ -614,7 +626,7 @@ function setupMonitor() {
|
|||||||
description,
|
description,
|
||||||
step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission'
|
step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission'
|
||||||
: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit'
|
: ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit'
|
||||||
: toolName,
|
: toolName,
|
||||||
step_index: si,
|
step_index: si,
|
||||||
source: 'step_probe',
|
source: 'step_probe',
|
||||||
});
|
});
|
||||||
@@ -731,7 +743,7 @@ function setupMonitor() {
|
|||||||
: fileContent;
|
: fileContent;
|
||||||
// Write as snapshot with attached_files for bot to send as Discord file
|
// Write as snapshot with attached_files for bot to send as Discord file
|
||||||
ctx.writeChatSnapshotWithFiles(
|
ctx.writeChatSnapshotWithFiles(
|
||||||
`📎 **문서: ${fileName}** (${Math.round(fileContent.length/1024)}KB)`,
|
`📎 **문서: ${fileName}** (${Math.round(fileContent.length / 1024)}KB)`,
|
||||||
[{ name: fileName, content: truncatedContent }]
|
[{ name: fileName, content: truncatedContent }]
|
||||||
);
|
);
|
||||||
ctx.logToFile(`[NOTIFY-STEP] relayed artifact: ${fileName} (${fileContent.length} chars)`);
|
ctx.logToFile(`[NOTIFY-STEP] relayed artifact: ${fileName} (${fileContent.length} chars)`);
|
||||||
@@ -797,14 +809,14 @@ function setupMonitor() {
|
|||||||
const sentAt = ctx.recentDiscordSentTexts.get(trimmed);
|
const sentAt = ctx.recentDiscordSentTexts.get(trimmed);
|
||||||
if (sentAt && (Date.now() - sentAt) < 60_000) {
|
if (sentAt && (Date.now() - sentAt) < 60_000) {
|
||||||
ctx.recentDiscordSentTexts.delete(trimmed);
|
ctx.recentDiscordSentTexts.delete(trimmed);
|
||||||
ctx.logToFile(`[USER-MSG] skipped echo relay (Discord origin, ${Math.round((Date.now()-sentAt)/1000)}s ago)`);
|
ctx.logToFile(`[USER-MSG] skipped echo relay (Discord origin, ${Math.round((Date.now() - sentAt) / 1000)}s ago)`);
|
||||||
} else if (umText.length > 2) {
|
} else if (umText.length > 2) {
|
||||||
// Content-based dedup: AG can create multiple USER_INPUT steps for the same message
|
// Content-based dedup: AG can create multiple USER_INPUT steps for the same message
|
||||||
// (e.g. comment-while-working feature). Skip if same text relayed within 30s.
|
// (e.g. comment-while-working feature). Skip if same text relayed within 30s.
|
||||||
const dedupKey = `user_msg:${trimmed}`;
|
const dedupKey = `user_msg:${trimmed}`;
|
||||||
const lastRelayed = lastSnapshotText.get(dedupKey);
|
const lastRelayed = lastSnapshotText.get(dedupKey);
|
||||||
if (lastRelayed && (Date.now() - Number(lastRelayed)) < 30_000) {
|
if (lastRelayed && (Date.now() - Number(lastRelayed)) < 30_000) {
|
||||||
ctx.logToFile(`[USER-MSG] skipped duplicate relay (same text ${Math.round((Date.now() - Number(lastRelayed))/1000)}s ago)`);
|
ctx.logToFile(`[USER-MSG] skipped duplicate relay (same text ${Math.round((Date.now() - Number(lastRelayed)) / 1000)}s ago)`);
|
||||||
} else {
|
} else {
|
||||||
lastSnapshotText.set(dedupKey, String(Date.now()));
|
lastSnapshotText.set(dedupKey, String(Date.now()));
|
||||||
const truncated = umText.length > 800
|
const truncated = umText.length > 800
|
||||||
@@ -858,7 +870,7 @@ function setupMonitor() {
|
|||||||
}
|
}
|
||||||
// Log first time to capture actual field names
|
// Log first time to capture actual field names
|
||||||
if (!textContent) {
|
if (!textContent) {
|
||||||
ctx.logToFile(`[RESPONSE-CAPTURE] plannerResponse keys: [${Object.keys(pr).join(',')}] values(100): ${JSON.stringify(pr).substring(0,200)}`);
|
ctx.logToFile(`[RESPONSE-CAPTURE] plannerResponse keys: [${Object.keys(pr).join(',')}] values(100): ${JSON.stringify(pr).substring(0, 200)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Extract from ephemeralMessage field
|
// Extract from ephemeralMessage field
|
||||||
@@ -1252,7 +1264,7 @@ async function processResponseFile(filePath: string) {
|
|||||||
|
|
||||||
|
|
||||||
/** Write a pending approval file matching Bot's ApprovalRequest dataclass. */
|
/** Write a pending approval file matching Bot's ApprovalRequest dataclass. */
|
||||||
export function writePendingApproval(data: { conversation_id: string; command: string; description: string; step_type?: string; step_index?: number; source?: string; buttons?: Array<{text: string; index: number}>; modified_files?: string[]; edit_step_indices?: number[] }) {
|
export function writePendingApproval(data: { conversation_id: string; command: string; description: string; step_type?: string; step_index?: number; source?: string; buttons?: Array<{ text: string; index: number }>; modified_files?: string[]; edit_step_indices?: number[] }) {
|
||||||
try {
|
try {
|
||||||
const pendingDir = path.join(ctx.bridgePath, 'pending');
|
const pendingDir = path.join(ctx.bridgePath, 'pending');
|
||||||
if (!fs.existsSync(pendingDir)) { fs.mkdirSync(pendingDir, { recursive: true }); }
|
if (!fs.existsSync(pendingDir)) { fs.mkdirSync(pendingDir, { recursive: true }); }
|
||||||
@@ -1459,7 +1471,7 @@ export async function tryApprovalStrategies(approved: boolean, sessionId: string
|
|||||||
}
|
}
|
||||||
// Direct LS RPC with correct method name
|
// Direct LS RPC with correct method name
|
||||||
try {
|
try {
|
||||||
ctx.logToFile(`[APPROVAL-CODE-EDIT] acknowledgeCodeActionStep(cascadeId=${sessionId.substring(0,8)}, accept=${approved}, stepIndices=[${effectiveStepIndex}])`);
|
ctx.logToFile(`[APPROVAL-CODE-EDIT] acknowledgeCodeActionStep(cascadeId=${sessionId.substring(0, 8)}, accept=${approved}, stepIndices=[${effectiveStepIndex}])`);
|
||||||
const ackResult = await ctx.sdk.ls.rawRPC('acknowledgeCodeActionStep', {
|
const ackResult = await ctx.sdk.ls.rawRPC('acknowledgeCodeActionStep', {
|
||||||
cascadeId: sessionId,
|
cascadeId: sessionId,
|
||||||
accept: approved,
|
accept: approved,
|
||||||
|
|||||||
Reference in New Issue
Block a user