fix(ext): !stop CancelCascadeInvocation RPC — AG 빨간■ 동일 메커니즘 적용 #task-411

This commit is contained in:
Variet Worker
2026-03-18 07:16:17 +09:00
parent 759dab55b6
commit d8eac80b2f
6 changed files with 685 additions and 656 deletions

View File

@@ -43,12 +43,10 @@
### [2026-03-11] rejectAgentStep — AG 미등록 VS Code 커맨드 ### [2026-03-11] rejectAgentStep — AG 미등록 VS Code 커맨드
- **증상**: `/stop` 및 거부 시 `antigravity.agent.rejectAgentStep``command not found` - **증상**: `/stop` 및 거부 시 `antigravity.agent.rejectAgentStep``command not found`
- **원인**: AG IDE가 이 커맨드를 런타임에 등록하지 않음 (상수 정의만 존재) - **원인**: AG IDE가 이 커맨드를 런타임에 등록하지 않음 (상수 정의만 존재)
- **해결** (2026-03-18): `command-handler.ts``!stop` 핸들러를 `sdk.cascade.cancelCurrentTask()`로 교체. - **해결** (2026-03-18): `_cancelCurrentCascade()` 헬퍼 추가 — `sdk.titles.getActiveCascadeId()``ls.cancelCascade(cascadeId)` (CancelCascadeInvocation RPC).
WS 경로는 이미 SDK 사용 중이었으므로 file-based 경로만 수정. AG의 빨간색 ■ 버튼과 동일한 메커니즘. rawRPC fallback 포함.
- `CancelCascadeInvocation` gRPC 메서드도 사용 가능 (cascade_id 필요) - ~~`sdk.cascade.cancelCurrentTask()` — SDK에 존재하지 않는 메서드, 무시됨~~
- **E2E 검증 필요** — AG 가동 중 `!stop` 명령 테스트 - **주의**: `getActiveCascadeId()`가 null이면 취소 불가 — 로그로 확인 필요
- **주의**: `sdk.cascade.rejectStep()`은 여전히 내부적으로 `rejectAgentStep` 커맨드를 호출할 수 있음.
단일 step 거부보다 `cancelCurrentTask()`(전체 중단)가 더 안정적.
- **Vikunja**: #411 - **Vikunja**: #411
--- ---

View File

@@ -4,4 +4,5 @@
|---|------|------|------|------| |---|------|------|------|------|
| 001 | 06:09~06:26 | known-issues 정리/아카이빙 + Collector 폐기 마킹 + 문서 보강 (architecture, tech-stack, conventions) | `881a424` | ✅ | | 001 | 06:09~06:26 | known-issues 정리/아카이빙 + Collector 폐기 마킹 + 문서 보강 (architecture, tech-stack, conventions) | `881a424` | ✅ |
| 002 | 06:33~06:50 | Hub/WS 단위 테스트 45개 작성 (연결 관리, pending_owners, 라우팅, 인증) | `ac803d4` | ✅ | | 002 | 06:33~06:50 | Hub/WS 단위 테스트 45개 작성 (연결 관리, pending_owners, 라우팅, 인증) | `ac803d4` | ✅ |
| 003 | 06:50~07:00 | rejectAgentStep 대안 조사 — CancelCascadeInvocation RPC 발견 | `bcca706` | 🔧 | | 003 | 06:50~07:05 | !stop 핸들러 SDK cancelCurrentTask() 교체 + VSIX 재빌드/설치 | `759dab5` | |
| 004 | 07:03~07:15 | !stop 재수정 — cancelCurrentTask→CancelCascadeInvocation RPC (AG 빨간■ 동일) + VSIX 설치 | `8d5940b` | ✅ |

View File

@@ -435,7 +435,7 @@ async function activate(context) {
onCommand: (data) => { onCommand: (data) => {
logToFile(`[WS-CMD] ${data.text?.substring(0, 50)}`); logToFile(`[WS-CMD] ${data.text?.substring(0, 50)}`);
(0, command_handler_1.handleWSCommand)({ (0, command_handler_1.handleWSCommand)({
bridgePath, projectName, sdk, autoApproveEnabled, logToFile, bridgePath, projectName, sdk, ls: sdk?.ls, autoApproveEnabled, logToFile,
onAutoApproveChanged: (enabled) => { autoApproveEnabled = enabled; }, onAutoApproveChanged: (enabled) => { autoApproveEnabled = enabled; },
recentDiscordSentTexts, recentDiscordSentTexts,
}, data); }, data);
@@ -558,7 +558,7 @@ async function activate(context) {
} }
// Watch commands directory // Watch commands directory
(0, command_handler_1.watchCommandsDir)({ (0, command_handler_1.watchCommandsDir)({
bridgePath, projectName, sdk, autoApproveEnabled, logToFile, bridgePath, projectName, sdk, ls: sdk?.ls, autoApproveEnabled, logToFile,
onAutoApproveChanged: (enabled) => { autoApproveEnabled = enabled; }, onAutoApproveChanged: (enabled) => { autoApproveEnabled = enabled; },
recentDiscordSentTexts, recentDiscordSentTexts,
}); });

File diff suppressed because one or more lines are too long

View File

@@ -17,6 +17,8 @@ export interface CommandHandlerContext {
bridgePath: string; bridgePath: string;
projectName: string; projectName: string;
sdk: any; sdk: any;
/** LSBridge instance for direct LS RPC calls (cancelCascade, etc.) */
ls: any;
autoApproveEnabled: boolean; autoApproveEnabled: boolean;
logToFile: (msg: string) => void; logToFile: (msg: string) => void;
/** Called when auto-approve is toggled; extension.ts updates its own state */ /** Called when auto-approve is toggled; extension.ts updates its own state */
@@ -93,9 +95,7 @@ export function handleWSCommand(ctx: CommandHandlerContext, data: { text?: strin
if (text === '!stop') { if (text === '!stop') {
ctx.logToFile('[WS-CMD] !stop — cancelling AG task'); ctx.logToFile('[WS-CMD] !stop — cancelling AG task');
if (ctx.sdk) { _cancelCurrentCascade(ctx);
try { ctx.sdk.cascade.cancelCurrentTask(); } catch { }
}
return; return;
} }
@@ -122,6 +122,42 @@ export function handleWSCommand(ctx: CommandHandlerContext, data: { text?: strin
// ─── Private ─── // ─── Private ───
/**
* Cancel the currently active cascade via CancelCascadeInvocation RPC.
* This is the same mechanism AG's native red ■ stop button uses.
*/
async function _cancelCurrentCascade(ctx: CommandHandlerContext) {
// 1. Get the active cascade ID from SDK titles manager
const cascadeId = ctx.sdk?.titles?.getActiveCascadeId?.();
if (!cascadeId) {
ctx.logToFile('[STOP] No active cascade to cancel (getActiveCascadeId returned null)');
return;
}
ctx.logToFile(`[STOP] Cancelling cascade: ${cascadeId.substring(0, 12)}...`);
// 2. Use LSBridge.cancelCascade() → CancelCascadeInvocation RPC
if (ctx.ls) {
try {
await ctx.ls.cancelCascade(cascadeId);
ctx.logToFile(`[STOP] ✅ CancelCascadeInvocation sent for ${cascadeId.substring(0, 12)}`);
return;
} catch (e: any) {
ctx.logToFile(`[STOP] LSBridge cancelCascade failed: ${e.message}`);
}
}
// 3. Fallback: try rawRPC directly via sdk.ls
if (ctx.sdk?.ls?.rawRPC) {
try {
await ctx.sdk.ls.rawRPC('CancelCascadeInvocation', { cascadeId });
ctx.logToFile(`[STOP] ✅ rawRPC CancelCascadeInvocation sent for ${cascadeId.substring(0, 12)}`);
} catch (e: any) {
ctx.logToFile(`[STOP] rawRPC fallback also failed: ${e.message}`);
}
}
}
function _processCommandFile(filePath: string, ctx: CommandHandlerContext) { function _processCommandFile(filePath: string, ctx: CommandHandlerContext) {
try { try {
const content = fs.readFileSync(filePath, 'utf-8'); const content = fs.readFileSync(filePath, 'utf-8');
@@ -158,15 +194,8 @@ function _processCommandFile(filePath: string, ctx: CommandHandlerContext) {
console.log(`Gravity Bridge: approve_terminal error: ${e.message}`) console.log(`Gravity Bridge: approve_terminal error: ${e.message}`)
); );
} else if (text === '!stop') { } else if (text === '!stop') {
// Cancel current operation — use SDK (rejectAgentStep is NOT a registered VS Code command) // Cancel current operation — use CancelCascadeInvocation RPC (same as AG's red ■ button)
if (ctx.sdk) { _cancelCurrentCascade(ctx);
try {
ctx.sdk.cascade.cancelCurrentTask();
console.log('Gravity Bridge: ✅ stop sent via SDK');
} catch (e: any) {
console.log(`Gravity Bridge: stop error: ${e.message}`);
}
}
} else if (text.startsWith('!auto')) { } else if (text.startsWith('!auto')) {
// Auto-approve mode toggle // Auto-approve mode toggle
let enabled: boolean; let enabled: boolean;

View File

@@ -366,6 +366,7 @@ let sawRunningAfterPending = true;
// ─── Step Probe, Response Watcher, Approval Strategies → extracted to ./step-probe.ts ─── // ─── Step Probe, Response Watcher, Approval Strategies → extracted to ./step-probe.ts ───
export async function activate(context: vscode.ExtensionContext) { export async function activate(context: vscode.ExtensionContext) {
console.log('Gravity Bridge: activating...'); console.log('Gravity Bridge: activating...');
@@ -426,7 +427,7 @@ export async function activate(context: vscode.ExtensionContext) {
onCommand: (data: WSCommandData) => { onCommand: (data: WSCommandData) => {
logToFile(`[WS-CMD] ${data.text?.substring(0, 50)}`); logToFile(`[WS-CMD] ${data.text?.substring(0, 50)}`);
handleWSCommand({ handleWSCommand({
handleWSCommand({ bridgePath, projectName, sdk, ls: sdk?.ls, autoApproveEnabled, logToFile,
onAutoApproveChanged: (enabled: boolean) => { autoApproveEnabled = enabled; }, onAutoApproveChanged: (enabled: boolean) => { autoApproveEnabled = enabled; },
recentDiscordSentTexts, recentDiscordSentTexts,
}, data); }, data);
@@ -555,7 +556,7 @@ export async function activate(context: vscode.ExtensionContext) {
// Watch commands directory // Watch commands directory
watchCommandsDir({ watchCommandsDir({
watchCommandsDir({ bridgePath, projectName, sdk, ls: sdk?.ls, autoApproveEnabled, logToFile,
onAutoApproveChanged: (enabled: boolean) => { autoApproveEnabled = enabled; }, onAutoApproveChanged: (enabled: boolean) => { autoApproveEnabled = enabled; },
recentDiscordSentTexts, recentDiscordSentTexts,
}); });