Files
pi-config/extensions/pi-crew/docs/refactor-tasks-phase3.md

395 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 3 Refactor Plan — Port utilities & patterns from `source/`
> Xuất xứ: review sâu `source/pi-subagents` và `source/pi-mono/packages/coding-agent` (28/04/2026).
> Mục tiêu: port các utility/pattern còn thiếu/yếu trong pi-crew để tăng độ ổn định, quan sát, và bảo trì.
> Phase 2 (#17#25) đã hoàn tất, baseline: tsc 0 errors, 176 unit + 21 integration pass.
## Quy ước chung
- Không phá vỡ public API hiện tại. Mọi thay đổi nội bộ.
- Sau mỗi task: `npx tsc --noEmit` + `npm run test:unit` (+ `test:integration` nếu liên quan watcher/IO).
- Không thêm dependency runtime mới trừ khi task ghi rõ.
- Mỗi task = 1 commit độc lập có thể revert. Đặt tên test bám sát hành vi.
## Trạng thái cập nhật
- [x] Task #26`completion-dedupe` (đã hoàn tất)
- [x] Task #27`jsonl-writer` (đã hoàn tất)
- [x] Task #28`post-exit-stdio-guard` (đã hoàn tất)
- [x] Task #29`sleep` (đã hoàn tất)
- [x] Task #30`timings` (đã hoàn tất)
- [x] Task #31`fs-watch` (đã hoàn tất)
- [x] Task #32`result-watcher` (đã hoàn tất)
- [x] Task #33`parallel-utils` (đã hoàn tất)
- [x] Task #34`artifact-cleanup` (đã hoàn tất)
- [x] Task #35`team-doctor` (đã hoàn tất)
- [x] Task #37`hosted-git-info` cho team config git URL (đã hoàn tất)
- [ ] Task #36`proper-lockfile` (đã tạm hoãn, giữ `locks.ts` nội bộ)
---
## Batch A — Low-risk utility ports (ưu tiên cao)
Mục tiêu: 6 file mới + 2 file điều chỉnh. Risk thấp, tách rõ, dễ test riêng. Ước tính: 12h.
### Task #26 — Port `completion-dedupe.ts`
**Source**: `source/pi-subagents/completion-dedupe.ts`
**Đích**: `pi-crew/src/utils/completion-dedupe.ts`
**Lý do**: Pi-crew chưa có TTL seen-map. Khi `result-watcher`/mailbox được restart hoặc `primeExistingResults` chạy đồng thời với event mới, có thể double-emit. TTL map + key xây từ `(sessionId, agent, timestamp, taskIndex, totalTasks, success)` đảm bảo idempotent trong khoảng TTL.
**API export**:
```typescript
export function buildCompletionKey(data: CompletionDataLike, fallback: string): string;
export function pruneSeenMap(seen: Map<string, number>, now: number, ttlMs: number): void;
export function markSeenWithTtl(seen: Map<string, number>, key: string, now: number, ttlMs: number): boolean;
export function getGlobalSeenMap(storeKey: string): Map<string, number>;
```
**Acceptance**:
- File copy nguyên vẹn (chỉ điều chỉnh import paths nếu cần).
- Unit test `test/unit/completion-dedupe.test.ts`: cover 4 case
- `buildCompletionKey` với `id` ưu tiên cao nhất
- `buildCompletionKey` với meta fallback (no id)
- `markSeenWithTtl` trả về `true` lần thứ 2 trong TTL
- `pruneSeenMap` xoá entry expired
- Tích hợp: callsite mới sẽ làm trong Task #27.
**Verification**: `npx tsc --noEmit` + `npm run test:unit -- --grep completion-dedupe`
---
### Task #27 — Port `jsonl-writer.ts` + tích hợp event-log
**Source**: `source/pi-subagents/jsonl-writer.ts`
**Đích**: `pi-crew/src/state/jsonl-writer.ts`
**Lý do**: Pi-crew `events.jsonl` không có cap; run dài có thể grow vô hạn. JSONL writer của pi-subagents có:
- Backpressure (`source.pause()`/`resume()` khi `stream.write()` trả false)
- Max bytes hardcap (default 50MB) — drop silently sau threshold
- Best-effort error handling (try/catch quanh `createWriteStream`)
**Tích hợp**:
1. `event-log.ts` hiện tại append synchronous via `fs.appendFileSync`. Đổi sang `createJsonlWriter` sẽ phải async writes → cần xem xét impact với `appendEvent` callsites.
2. Phương án ít rủi ro: KHÔNG đổi `event-log.ts` đường nóng synchronous. Thay vào đó:
- Thêm size check trong `appendEvent`: trước khi append, `fs.statSync(eventsFile)` → nếu > `MAX_EVENTS_BYTES` (default 50MB) → log warning + drop.
- Hoặc rotation: rename `events.jsonl``events.jsonl.1` khi vượt threshold.
**API export**:
```typescript
export function createJsonlWriter(filePath: string | undefined, source: DrainableSource, deps?: JsonlWriterDeps): JsonlWriter;
```
**Acceptance**:
- File copy với điều chỉnh path imports.
- Unit test `test/unit/jsonl-writer.test.ts`: cover 4 case
- Writes line + newline
- Drops line khi vượt `maxBytes`
- Pause/resume source khi backpressure
- `close()` flush stream
- Tích hợp `event-log.ts`: thêm size guard (KHÔNG đổi sync→async). Nếu `events.jsonl` > `MAX_EVENTS_BYTES`, log internal-error + skip append (giữ nguyên runtime).
**Risk**: Thay đổi `event-log.ts` là đường nóng. Test integration `live-mailbox-flow` để đảm bảo không regress.
**Verification**: `npx tsc --noEmit` + `npm run test:unit` + `npm run test:integration`
---
### Task #28 — Tách `post-exit-stdio-guard` thành module riêng
**Source**: `source/pi-subagents/post-exit-stdio-guard.ts`
**Đích**: `pi-crew/src/runtime/post-exit-stdio-guard.ts`
**Lý do**: `child-pi.ts` hiện inline 60+ dòng quản lý timer post-exit. Tách module → tái dùng cho subagent + worker, dễ unit test.
**API export**:
```typescript
export function attachPostExitStdioGuard(
child: ChildWithPipedStdio,
options: { idleMs: number; hardMs: number },
): () => void;
export function trySignalChild(child: ChildWithKill, signal: NodeJS.Signals): boolean;
```
**Tích hợp**:
- Trong `child-pi.ts`:
- Thay block `postExitGuard = setTimeout(...)` + `child.stdout?.destroy()` bằng `attachPostExitStdioGuard(child, { idleMs: POST_EXIT_STDIO_GUARD_MS, hardMs: HARD_KILL_MS })`.
- Cleanup function được gọi trong `settle()`.
- Giữ logic `noResponseTimer` + `finalDrainTimer` riêng (chúng là khác semantics — pre-exit, không phải post-exit).
**Acceptance**:
- `runChildPi` test hiện có vẫn pass.
- Thêm unit test `test/unit/post-exit-stdio-guard.test.ts`: simulate child exit + dangling stdout → verify destroy gọi sau idleMs.
- Behaviour: khi child không exit nhưng stdio idle → KHÔNG destroy (chỉ destroy sau exit).
**Verification**: `npx tsc --noEmit` + `npm run test:unit -- --grep child-pi` + `npm run test:unit -- --grep post-exit`
---
### Task #29 — Port `utils/sleep.ts`
**Source**: `source/pi-mono/packages/coding-agent/src/utils/sleep.ts`
**Đích**: `pi-crew/src/utils/sleep.ts`
**Lý do**: Abortable sleep helper. Hữu ích cho retry/backoff trong `model-fallback.ts`, `task-runner.ts`, `subagent-manager.ts` (`scheduleStuckBlockedNotify`).
**API export**:
```typescript
export function sleep(ms: number, signal?: AbortSignal): Promise<void>;
```
**Tích hợp** (không bắt buộc lần đầu, chỉ port file):
- Quét `setTimeout(...{}, ms)` patterns trong `model-fallback.ts` để đánh giá có thay không. Mặc định KHÔNG đổi callsite trong task này — file utility độc lập.
**Acceptance**:
- File copy nguyên vẹn.
- Unit test `test/unit/sleep.test.ts`: 3 case
- Resolve sau ms
- Reject ngay nếu signal đã abort
- Reject khi abort trong lúc đợi + clear timeout
**Verification**: `npx tsc --noEmit` + `npm run test:unit -- --grep sleep`
---
### Task #30 — Port `core/timings.ts` (PI_TIMING profiler)
**Source**: `source/pi-mono/packages/coding-agent/src/core/timings.ts`
**Đích**: `pi-crew/src/utils/timings.ts`
**Lý do**: Pi-crew register nhiều slash command/widget/extension hooks. Khi user báo "khởi động chậm", hiện tại không có cách nhanh để đo. `PI_TIMING=1` env → in breakdown từng giai đoạn.
**API export**:
```typescript
export function resetTimings(): void;
export function time(label: string): void;
export function printTimings(): void;
```
**Tích hợp**:
- Trong `index.ts` / `src/extension/register.ts`:
- Đầu file: `import { time, printTimings, resetTimings } from "./utils/timings.js"`.
- Sau từng bước register lớn (load config, register tools, register slash commands, register widgets, init runtime resolver): `time("step-name")`.
- Cuối: gọi `printTimings()` (no-op nếu không bật env).
**Acceptance**:
- File copy nguyên vẹn.
- Unit test minimal: gọi `time` + `printTimings` không throw.
- Smoke: `PI_TIMING=1 node --experimental-strip-types -e "import('./pi-crew/index.ts')"` in ra `--- Startup Timings ---`.
**Verification**: `npx tsc --noEmit` + manual smoke với `PI_TIMING=1`.
---
### Task #31 — Port `utils/fs-watch.ts`
**Source**: `source/pi-mono/packages/coding-agent/src/utils/fs-watch.ts`
**Đích**: `pi-crew/src/utils/fs-watch.ts`
**Lý do**: Wrapper an toàn cho `fs.watch` với:
- `closeWatcher(watcher)`: nuốt error khi close
- `watchWithErrorHandler(path, listener, onError)`: try/catch quanh `watch()`, tự gọi `onError` nếu throw, attach `error` listener
**API export**:
```typescript
export const FS_WATCH_RETRY_DELAY_MS: number;
export function closeWatcher(watcher: FSWatcher | null | undefined): void;
export function watchWithErrorHandler(path: string, listener: WatchListener<string>, onError: () => void): FSWatcher | null;
```
**Tích hợp** (không bắt buộc lần đầu, chỉ port file):
- Khi viết `result-watcher` (Task #32 Tier 2), dùng wrapper này.
**Acceptance**:
- File copy.
- Unit test `test/unit/fs-watch.test.ts`: 2 case
- `closeWatcher(null)` không throw
- `watchWithErrorHandler` gọi `onError` khi `watch()` throw (mock fs)
**Verification**: `npx tsc --noEmit` + `npm run test:unit -- --grep fs-watch`
---
## Batch B — Pattern lớn hơn, cần thiết kế
Mục tiêu: 3 task có thiết kế. Risk trung bình. Ước tính: 34h.
### Task #32 — Result watcher auto-restart pattern
**Source**: `source/pi-subagents/result-watcher.ts`
**Đích**: `pi-crew/src/runtime/result-watcher.ts` (mới) HOẶC tích hợp vào mailbox/event-log nếu phù hợp.
**Lý do**: Khi `fs.watch` báo error (filesystem bị unmount, network drive disconnect), pi-crew hiện không tự khôi phục. Pattern: bắt error → setTimeout 3s → mkdir + start lại watcher.
**Phụ thuộc**: Task #31 (fs-watch), Task #26 (completion-dedupe).
**API export**:
```typescript
export function createResultWatcher(input: {
resultsDir: string;
onResult: (file: string) => Promise<void>;
state: ResultWatcherState;
completionTtlMs: number;
}): {
start: () => void;
primeExisting: () => void;
stop: () => void;
};
```
**Acceptance**:
- Unit test:
- Watcher emits scheduled file → `onResult` được gọi.
- Watcher error → 3s sau tự restart (dùng fake timers).
- Dedupe: 2 events cùng file trong TTL → `onResult` chỉ gọi 1 lần.
- Integration test với fixture `tmp/results/`: write file → onResult chạy → file unlink.
**Risk**: Pi-crew có thể chưa có "result file producer" pattern (results đang qua mailbox in-process). Đánh giá: nếu KHÔNG có async result file pattern, **bỏ qua task này**.
**Verification**: `npm run test:unit` + `npm run test:integration`
---
### Task #33 — Port `parallel-utils` (mapConcurrent + aggregateParallelOutputs)
**Source**: `source/pi-subagents/parallel-utils.ts`
**Đích**: `pi-crew/src/runtime/parallel-utils.ts`
**Lý do**:
- `concurrency.ts` chỉ tính toán số concurrent, không có helper map.
- `parallel-research.ts` hiện viết riêng worker pool. Có thể đơn giản hoá.
- `aggregateParallelOutputs` chuẩn hoá format kết quả (FAILED/SKIPPED/EMPTY OUTPUT) — pi-crew có thể tận dụng cho task summary.
**API export**:
```typescript
export async function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, i: number) => Promise<R>): Promise<R[]>;
export interface ParallelTaskResult { agent: string; taskIndex?: number; output: string; exitCode: number | null; error?: string; ... }
export function aggregateParallelOutputs(results: ParallelTaskResult[], headerFormat?: ...): string;
export const MAX_PARALLEL_CONCURRENCY: number;
```
**Tích hợp**:
- Refactor `parallel-research.ts` dùng `mapConcurrent` (giữ behaviour).
- Xét dùng trong `task-graph-scheduler.ts` cho batches ready tasks.
**Acceptance**:
- Unit test `test/unit/parallel-utils.test.ts`:
- `mapConcurrent` tôn trọng limit (counter pending max).
- `mapConcurrent([], 4, fn)` trả `[]`, không gọi fn.
- `mapConcurrent` propagate exception.
- `aggregateParallelOutputs` format đúng cho 4 case (success/failed/skipped/empty).
**Verification**: `npm run test:unit -- --grep parallel-utils`
---
### Task #34 — Artifact cleanup với daily marker
**Source**: `source/pi-subagents/artifacts.ts` (hàm `cleanupOldArtifacts`)
**Đích**: bổ sung vào `pi-crew/src/state/artifact-store.ts`
**Lý do**: Pi-crew `<crewRoot>/state/artifacts/` (`<crewRoot>` = `.crew/` mới hoặc `.pi/teams/` legacy) không có TTL → run cũ tích lũy mãi. Pattern subagents:
- File `.last-cleanup` chứa timestamp.
- Nếu marker mới hơn 24h → skip (không scan dir lớn mỗi extension load).
- Nếu cần scan: xoá file mtime > `maxAgeDays * 24h`.
**API mới trong artifact-store.ts**:
```typescript
export function cleanupOldArtifacts(artifactsRoot: string, maxAgeDays: number): void;
```
**Tích hợp**:
- Gọi 1 lần khi extension activate, sau khi resolve `artifactsRoot`.
- Default: `maxAgeDays = 7` (config qua `defaults.ts`).
- Xét cleanup `events.jsonl` cũ tương tự (có rotation pattern Task #27).
**Acceptance**:
- Unit test `test/unit/artifact-cleanup.test.ts`:
- Tạo files với mtime cũ + mới → cleanup chỉ xoá cũ.
- Marker mới (< 24h) → skip cleanup.
- Marker cũ (> 24h) → scan + update marker.
- Dir không tồn tại → no-op.
- Tích hợp test (optional): activate extension 2 lần liên tiếp → lần 2 không scan.
**Verification**: `npm run test:unit -- --grep artifact-cleanup`
---
### Task #35 — Build `team doctor` action
**Source**: `source/pi-subagents/doctor.ts`
**Đích**: `pi-crew/src/extension/team-tool/doctor.ts` (mới) + register trong team-tool.
**Lý do**: Pi-crew thiếu lệnh diagnostic 1-liên-1. Format report của subagents có cấu trúc:
- Runtime (cwd, async, session)
- Filesystem (state/artifacts/runs dirs)
- Discovery (agents, teams, workflows count theo source)
- Configuration validation status
- Optional: intercom/extension status
**API**:
```typescript
export function buildTeamDoctorReport(input: {
cwd: string;
config: ResolvedConfig;
...
}): string;
```
**Tích hợp**:
- Thêm action `doctor` trong `team-tool` action handler.
- Slash command `/team-doctor` (nếu phù hợp với UX).
**Acceptance**:
- Unit test:
- Report có heading đúng.
- Filesystem section hiển thị "ok" cho dir tồn tại, "missing" cho không.
- Discovery counts khớp với fixture builtin/user/project.
- Khi exception trong section → in `failed — <error>` thay vì throw.
- Manual: chạy `team` action `doctor` → verify output text.
**Verification**: `npm run test:unit -- --grep doctor`
---
## Tier 3 — Library swaps (cân nhắc, không bắt buộc Phase 3)
### Task #36 (optional) — Đánh giá `proper-lockfile`
**Bối cảnh**: `source/pi-mono/packages/coding-agent/package.json` đã dùng `proper-lockfile`. Pi-crew tự viết `locks.ts` với O_EXCL + retry.
**Quyết định**:
- Nếu phát hiện flake/race trong `npm run test:integration` (đặc biệt `locks-race.test.ts`) → adopt.
- Nếu hiện tại pass ổn định → giữ `locks.ts` để zero-dep.
**Action nếu adopt**:
1. `npm install proper-lockfile @types/proper-lockfile`.
2. Replace `locks.ts` `acquireLock`/`releaseLock` bằng `lockfile.lock(filePath, { retries: ..., stale: ... })`.
3. Re-run `locks-race.test.ts` 100 iterations để xác nhận no regress.
**Verification**: full CI.
---
### Task #37 (optional) — `hosted-git-info` cho team config git URL
**Bối cảnh**: Khi pi-crew hỗ trợ `team: git+https://github.com/org/teams-repo` → dùng `parseGitUrl` của coding-agent.
**Trạng thái**: Đã triển khai cho runtime discover/validate: `ResourceSource` mở rộng thành `git`, `TeamConfig.sourceUrl` được ghi, parser `parseGitUrl` đã chuẩn hóa `git+` và hỗ trợ `#` ref.
---
## Tracking template (sao chép vào commit message)
```
Phase 3 #NN — <short title>
Source: source/pi-subagents/<file>.ts (or pi-mono/...)
Target: pi-crew/src/<dir>/<file>.ts
Risk: low | medium | high
Tests added: test/unit/<file>.test.ts
Verification: tsc --noEmit OK; test:unit OK; test:integration <OK|N/A>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
```
---
## Thứ tự gợi ý thực hiện
1. **Tuần 1 — Batch A (low-risk)**: #29#30#31#26#28#27
- Bắt đầu bằng `sleep`/`timings`/`fs-watch` (đơn lẻ, no callsite change).
- Tiếp `completion-dedupe` (file độc lập).
- Cuối `post-exit-stdio-guard` (chỉnh `child-pi.ts`) và `jsonl-writer` (chỉnh `event-log.ts`).
2. **Tuần 2 — Batch B (mid-risk)**: #33#34#35 → (#32 nếu áp dụng).
3. **Tuần 3 — Tier 3 nếu cần**: #36/#37 only on demand.
Toàn bộ Phase 3 ước tính 46h focus work, không thêm runtime dep ngoại trừ tuỳ chọn `proper-lockfile`.