feat: v2.2.92 → v2.2.158 — god-file 분해 + Stocks feature + 대화 연속성
R56–R59: agent.ts 2731→1529줄 god-file 분해 (25 modules) · attrParsers + LLM 메서드 8개 (callNonStreaming, streamChatOnce 등) · executeActions 415줄 → 8 handler 그룹 (file/run/list/brain/calendar/sheets/tasks) · handlePrompt 1100줄 → 7 phase 모듈 (system prompt + budget + autoContinue 등) R50–R55: extension.ts 1145→349줄 (telegram/settings/provider commands 분리) Stocks feature 신규: /stocks slash command (v2.2.152~158) · .astra/stocks.json 저장소 + Yahoo Finance 현재가 갱신 · 8 키워드 필터 (ROE/성장성/유동성/수익성/영업효율/기술력/안정성/PBR) · Naver 시가총액 페이지 JSON API (m.stock.naver.com) 발굴 · LLM Top 5 매력도 분석 + Telegram 자동 보고서 · KST 09:00/15:00 watcher 자동 모니터링 대화 연속성 (v2.2.150~157): · [PRIOR TURN CONCLUSION] block 으로 직전 결론 anchor · thin follow-up 분류 → boilerplate 헤더 suppression · slash 명령 결과 chatHistory mirror (capture wrapper) · echo/parrot 금지 system prompt rule 기타: /stocks 슬래시 자동완성 dropdown UI, Naver JSON API 전환 (cheerio 제거) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+41
-14
@@ -1,43 +1,67 @@
|
||||
import { logInfo } from '../utils';
|
||||
|
||||
/**
|
||||
* AsyncLockManager: Prevents race conditions by ensuring only one task
|
||||
* AsyncLockManager: Prevents race conditions by ensuring only one task
|
||||
* can access a specific resource (e.g., a file path) at a time.
|
||||
*
|
||||
*
|
||||
* Includes timeout protection to prevent indefinite lock-waiting,
|
||||
* and proper cleanup on acquisition failure.
|
||||
*
|
||||
* ─ v2 (unique token) ─────────────────────────────────────────────────────
|
||||
* 옛 구현은 cleanup 분기에서 `this.locks.get(resourceId) === previousLock.then(() => newLock)`
|
||||
* 으로 *Promise 객체 동일성* 을 비교했는데, `.then(...)` 은 매 호출마다 *새 Promise
|
||||
* instance* 를 반환해서 사실상 *항상 false* — cleanup 이 안 됨. 또 release 시점의
|
||||
* `delete(resourceId)` 도 latest 검증 없이 무조건 호출돼서, 같은 resource 에 연쇄
|
||||
* 호출이 있으면 다른 task 의 entry 를 silent 로 지우는 race.
|
||||
*
|
||||
* 각 entry 에 고유 symbol token 을 부여하고, cleanup / release 시 *내 token 이 아직
|
||||
* Map 의 latest 인지* 비교해서 안전하게 정리한다.
|
||||
*/
|
||||
|
||||
interface LockEntry {
|
||||
/** Previous lock chain + new lock — await 대상. */
|
||||
promise: Promise<void>;
|
||||
/** 이 entry 의 고유 식별자 — cleanup 시 자기 것만 지우게. */
|
||||
token: symbol;
|
||||
}
|
||||
|
||||
export class AsyncLockManager {
|
||||
private locks: Map<string, Promise<void>> = new Map();
|
||||
private locks: Map<string, LockEntry> = new Map();
|
||||
private static readonly DEFAULT_TIMEOUT_MS = 30_000;
|
||||
|
||||
/**
|
||||
* Acquires a lock for a specific resource.
|
||||
* If the resource is already locked, waits until the previous task finishes.
|
||||
* Times out after `timeoutMs` to prevent deadlocks.
|
||||
*
|
||||
*
|
||||
* @returns A release function that MUST be called when the work is done (use try/finally).
|
||||
*/
|
||||
public async acquire(resourceId: string, timeoutMs: number = AsyncLockManager.DEFAULT_TIMEOUT_MS): Promise<() => void> {
|
||||
const previousLock = this.locks.get(resourceId) || Promise.resolve();
|
||||
|
||||
const previousEntry = this.locks.get(resourceId);
|
||||
const previousPromise = previousEntry?.promise ?? Promise.resolve();
|
||||
|
||||
const token = Symbol(`lock:${resourceId}`);
|
||||
let release: () => void;
|
||||
const newLock = new Promise<void>((resolve) => {
|
||||
const newPromise = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
|
||||
this.locks.set(resourceId, previousLock.then(() => newLock));
|
||||
const myEntry: LockEntry = {
|
||||
promise: previousPromise.then(() => newPromise),
|
||||
token,
|
||||
};
|
||||
this.locks.set(resourceId, myEntry);
|
||||
|
||||
// Wait for previous lock with a timeout to prevent deadlocks
|
||||
// Wait for previous lock with a timeout to prevent deadlocks.
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error(`Lock acquisition timed out for resource: ${resourceId}`)), timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.race([previousLock, timeoutPromise]);
|
||||
await Promise.race([previousPromise, timeoutPromise]);
|
||||
} catch (error) {
|
||||
// Clean up the dangling lock on timeout
|
||||
if (this.locks.get(resourceId) === previousLock.then(() => newLock)) {
|
||||
// 내 token 이 아직 latest 면만 정리 — newer entry 가 있으면 그 task 가 관리.
|
||||
if (this.locks.get(resourceId)?.token === token) {
|
||||
this.locks.delete(resourceId);
|
||||
}
|
||||
release!();
|
||||
@@ -49,8 +73,11 @@ export class AsyncLockManager {
|
||||
return () => {
|
||||
logInfo(`Lock released for: ${resourceId}`);
|
||||
release();
|
||||
// Clean up the Map entry if this is the latest lock
|
||||
this.locks.delete(resourceId);
|
||||
// 내 token 이 latest 일 때만 Map 정리 — newer entry 가 등록돼 있으면
|
||||
// 그 task 가 자기 release 시 정리. 옛 코드는 무조건 delete 해서 race.
|
||||
if (this.locks.get(resourceId)?.token === token) {
|
||||
this.locks.delete(resourceId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user