Release v2.2.1: Autonomous Task Resumption & Engine Resilience

This commit is contained in:
g1nation
2026-05-14 23:27:51 +09:00
parent e86e3177c7
commit cd22da8735
21 changed files with 848 additions and 126 deletions
+63 -11
View File
@@ -38,8 +38,12 @@ import { detectProjectIntent, KnownProject } from './features/projectArchitectur
import {
readCompanyState,
runCompanyTurn,
resumeCompanyTurn,
listResumableSessions,
summarizeForChip,
CompanyTurnEvent,
DispatcherDeps,
ApprovalDecision,
COMPANY_AGENTS,
COMPANY_AGENT_ORDER,
ROLE_CATEGORY_LABELS,
@@ -1680,7 +1684,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
* progress events back as `companyTurnUpdate` messages so the same bubble
* fills in as each agent finishes.
*/
async _runCompanyTurn(userPrompt: string): Promise<void> {
async _runCompanyTurn(userPrompt: string, resumeTimestamp?: string): Promise<void> {
const cfg = getConfig();
const ai = new AIService();
const emit = (event: CompanyTurnEvent) => {
@@ -1692,7 +1696,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
const abort = new AbortController();
this._companyAbort = abort;
try {
await runCompanyTurn(userPrompt, {
const deps: DispatcherDeps = {
context: this._context,
ai,
defaultModel: cfg.defaultModel || 'gemma4:e2b',
@@ -1706,21 +1710,36 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
// executor so specialist outputs like `<create_file>` actually
// hit disk. Without this, agents would *claim* to create
// files while nothing happened — the exact bug we just fixed.
executeActionTags: (text) => this._agent.executeActionTagsOnText(text),
executeActionTags: (text: string) => this._agent.executeActionTagsOnText(text),
signal: abort.signal,
onEvent: emit,
// 승인 게이트 bridge — dispatcher가 호출하면 Promise를 만들어
// resolver를 _pendingApprovals에 보관 후 await. 사용자가 카드 버튼을
// 누르면 chatHandlers가 resolveApprovalGate(stageId, decision)을 호출
// 하고 그 resolve가 이 await을 풀어준다.
awaitApproval: ({ stageId }) => new Promise((resolve) => {
if (abort.signal.aborted) {
resolve({ kind: 'abort' });
return;
}
this._pendingApprovals.set(stageId, resolve);
}),
});
awaitApproval: ({ stageId }: { stageId: string; stageLabel: string }) =>
new Promise<ApprovalDecision>((resolve) => {
if (abort.signal.aborted) {
resolve({ kind: 'abort' });
return;
}
this._pendingApprovals.set(stageId, resolve);
}),
};
// 일반 새 turn vs 재개 turn 분기. 재개 시 _resume.json에서 plan + cursor를
// 복원해 그 다음 stage부터 dispatch가 이어진다. resumeCompanyTurn이 null을
// 돌려주면(파일 없음·이미 완료 등) 사용자에게 알리고 종료.
if (resumeTimestamp) {
const result = await resumeCompanyTurn(resumeTimestamp, deps);
if (!result) {
this._view?.webview.postMessage({
type: 'error',
value: '재개 가능한 세션 정보를 찾지 못했습니다 (이미 완료되었거나 파일이 손상되었을 수 있습니다).',
});
}
} else {
await runCompanyTurn(userPrompt, deps);
}
} catch (e: any) {
logError('company.runTurn: unexpected failure.', { error: e?.message ?? String(e) });
this._view?.webview.postMessage({
@@ -1737,6 +1756,39 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
// with the red Stop button after the round completes.
this._view?.webview.postMessage({ type: 'streamEnd' });
void this._sendReadyStatus();
// turn이 끝났으면(완료든 abort든) resume 가능 세션 목록을 새로 푸시 —
// 방금 abort된 세션이 곧장 목록에 떠야 하므로.
void this._sendCompanyResumable();
}
}
/**
* Webview에 "이어서 진행할 수 있는 세션" 목록을 push. 관리 패널이 열릴 때와 turn이
* 끝날 때마다 호출됨. 빈 목록도 그대로 보내서 UI가 섹션을 자동으로 숨길 수 있게 함.
*/
async _sendCompanyResumable(): Promise<void> {
if (!this._view) return;
try {
const items = listResumableSessions(this._context).map((s) => ({
timestamp: s.timestamp,
userPrompt: s.userPrompt.slice(0, 200),
pipelineId: s.pipelineId,
pipelineName: s.pipelineId
? (readCompanyState(this._context).pipelines?.[s.pipelineId]?.name ?? s.pipelineId)
: null,
completedCount: s.agentOutputs.length,
totalCount: s.plan.tasks.length,
status: s.status,
abortReason: s.abortReason ?? '',
lastUpdatedAt: s.lastUpdatedAt,
startedAt: s.startedAt,
}));
this._view.webview.postMessage({
type: 'companyResumable',
value: { items },
});
} catch (e: any) {
logError('company._sendCompanyResumable failed.', { error: e?.message ?? String(e) });
}
}