agy-mcp-bridge 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -423
- package/config/workspaces.example.json +4 -0
- package/dist/src/agents/antigravity.js +1 -1
- package/dist/src/agents/registry.js +1 -1
- package/dist/src/antigravity/process.js +5 -3
- package/dist/src/config.js +9 -0
- package/dist/src/isolation/orphans.js +38 -2
- package/dist/src/isolation/worktree.js +94 -11
- package/dist/src/models.js +2 -2
- package/dist/src/runtime.js +18 -7
- package/dist/src/tasks/manager.js +20 -3
- package/docs/VERIFICATION.md +2 -0
- package/package.json +2 -1
- package/ui/app.css +10 -2
- package/ui/app.js +105 -44
- package/docs/HANDOFF.md +0 -152
package/ui/app.js
CHANGED
|
@@ -56,15 +56,20 @@ function duration(ms) {
|
|
|
56
56
|
return s < 60 ? `${s}s` : `${Math.floor(s / 60)}p ${s % 60}s`;
|
|
57
57
|
}
|
|
58
58
|
function agoEl(iso, className) { return h('span', {class: className, 'data-ago': iso, title: clock(iso), text: ago(iso)}); }
|
|
59
|
-
/**
|
|
59
|
+
/** Secondary line under the progress bar; the "why" lives in ACTIVITY_HINT, shown as a tooltip. */
|
|
60
|
+
const ACTIVITY_HINT = 'Agy ở chế độ print/json chỉ in khi xong, nên "chưa có output" là bình thường. ' +
|
|
61
|
+
'Log của agy lớn dần là dấu hiệu tiến trình còn sống; khi còn hoạt động, bridge gia hạn hạn mềm thay vì cắt đúng lúc hết giờ.';
|
|
60
62
|
function activityText(activity) {
|
|
61
63
|
if (!activity) return 'Chưa có thông tin hoạt động.';
|
|
62
64
|
const parts = [];
|
|
63
|
-
if (activity.log_updated_at) parts.push(`
|
|
64
|
-
else if (activity.soft_deadline) parts.push('
|
|
65
|
-
parts.push(activity.last_output_at ? `
|
|
65
|
+
if (activity.log_updated_at) parts.push(`log agy: ${ago(activity.log_updated_at)} (${bytes(activity.log_bytes ?? 0)})`);
|
|
66
|
+
else if (activity.soft_deadline) parts.push('log agy: chưa có dữ liệu');
|
|
67
|
+
parts.push(activity.last_output_at ? `output cuối ${ago(activity.last_output_at)}` : 'chưa có output');
|
|
66
68
|
const soft = activity.soft_deadline;
|
|
67
|
-
if (soft)
|
|
69
|
+
if (soft) {
|
|
70
|
+
if (soft.extensions) parts.push(`đã gia hạn ${soft.extensions} lần`);
|
|
71
|
+
else parts.push(soft.next_check_at ? `chưa gia hạn, kiểm tra lại sau ${Math.max(0, Math.round((Date.parse(soft.next_check_at) - Date.now()) / 1000))}s` : 'chưa gia hạn');
|
|
72
|
+
}
|
|
68
73
|
return parts.join(' · ');
|
|
69
74
|
}
|
|
70
75
|
/** Steps agy reported (step progress setting): newest first, each with the raw event (redacted) one click away. */
|
|
@@ -258,6 +263,11 @@ const USAGE_LABELS = {input_tokens: 'Token vào', output_tokens: 'Token ra', tot
|
|
|
258
263
|
const usageName = key => USAGE_LABELS[key] ? `${USAGE_LABELS[key]} (${key})` : key;
|
|
259
264
|
const number = value => Number(value).toLocaleString('vi-VN', {maximumFractionDigits: 2});
|
|
260
265
|
const usageLine = usage => usage && Object.keys(usage).length ? Object.entries(usage).map(([key, value]) => `${key} ${number(value)}`).join(' · ') : null;
|
|
266
|
+
/** One number in the row, the whole breakdown one hover away. */
|
|
267
|
+
const usageBrief = usage => {
|
|
268
|
+
if (!usage || !Object.keys(usage).length) return '—';
|
|
269
|
+
return 'total_tokens' in usage ? `${number(usage.total_tokens)} token` : `${Object.keys(usage).length} trường usage`;
|
|
270
|
+
};
|
|
261
271
|
async function copy(text) {
|
|
262
272
|
try { await navigator.clipboard.writeText(text); notify('Đã sao chép'); } catch { notify('Không thể sao chép tự động'); }
|
|
263
273
|
}
|
|
@@ -304,7 +314,7 @@ function taskItems(o) {
|
|
|
304
314
|
class: 'task-item', role: 'listitem', 'data-task': task.task_id, 'aria-current': String(task.task_id === state.selected), onclick: () => select(task.task_id)
|
|
305
315
|
}, h('div', {class: 'row'}, pill(task.status), agoEl(task.queued_at || task.started_at, 'meta')),
|
|
306
316
|
h('div', {class: 'title', text: task.title || '(không có nội dung yêu cầu)'}),
|
|
307
|
-
h('div', {class: 'meta mono', text: `${task.agent}${task.isolation === 'worktree' ? ' · worktree' : ''} · ${folder(task.
|
|
317
|
+
h('div', {class: 'meta mono', text: `${task.agent}${task.isolation === 'worktree' ? ' · worktree' : ''} · ${folder(task.workspace)}`, title: task.task_id}),
|
|
308
318
|
h('div', {class: 'meta'}, task.status === 'running' ? elapsedEl(task.started_at, 'Đang chạy ') : task.status === 'queued' ? 'Đang xếp hàng…' : `Mất ${duration(task.duration_ms)}`,
|
|
309
319
|
task.tokens !== null && task.tokens !== undefined ? h('span', {class: overTask(o, task) ? 'over-budget' : '', text: ` · ${number(task.tokens)} token${overTask(o, task) ? ' (vượt mức)' : ''}`}) : null)));
|
|
310
320
|
}
|
|
@@ -315,6 +325,13 @@ function details(key, title, ...body) {
|
|
|
315
325
|
el.addEventListener('toggle', () => { el.open ? state.open.add(key) : state.open.delete(key); });
|
|
316
326
|
return el;
|
|
317
327
|
}
|
|
328
|
+
/** Text that answers "why / what if" instead of "what is happening now" gets collapsed until asked for. */
|
|
329
|
+
function collapsible(key, title, ...body) {
|
|
330
|
+
const el = details(key, title, ...body);
|
|
331
|
+
el.classList.add('more');
|
|
332
|
+
return el;
|
|
333
|
+
}
|
|
334
|
+
function more(key, ...body) { return collapsible(key, 'Xem thêm', ...body); }
|
|
318
335
|
|
|
319
336
|
const CHANGE_LABEL = {modified: 'Sửa', deleted: 'Xóa', added: 'Thêm', 'type-changed': 'Đổi kiểu'};
|
|
320
337
|
function changesBody(task) {
|
|
@@ -361,6 +378,21 @@ function scopeBody(task) {
|
|
|
361
378
|
return h('div', {class: 'callout', text: `${scope.changed} file thay đổi, tất cả nằm trong owns.`});
|
|
362
379
|
}
|
|
363
380
|
|
|
381
|
+
/**
|
|
382
|
+
* What the bridge did with the processes the agent left behind. Absent while the task runs and while the release
|
|
383
|
+
* query is still open, so a missing line is not a failure to clean up.
|
|
384
|
+
*/
|
|
385
|
+
function cleanupBody(task) {
|
|
386
|
+
const c = task.cleanup;
|
|
387
|
+
if (!c) return null;
|
|
388
|
+
if (!c.checked) return h('div', {class: 'callout warn', text: `Không kiểm tra được cây tiến trình agent: ${c.reason || 'lỗi không xác định'}`});
|
|
389
|
+
const parts = [c.reaped.length ? `đã giải phóng ${c.reaped.length} cây tiến trình (${c.reaped.join(', ')})` : 'không còn cây tiến trình nào cần dọn'];
|
|
390
|
+
if (c.skipped.length) parts.push(`${c.skipped.length} nhánh vẫn chạy: ${c.skipped.map(s => `${s.pid} — ${s.guard}`).join('; ')}`);
|
|
391
|
+
if (typeof c.tracked === 'number') parts.push(`đã ghi danh ${c.tracked} tiến trình trong lúc agent chạy`);
|
|
392
|
+
if (c.group_signalled === true) parts.push('đã gửi tín hiệu tới cả nhóm tiến trình');
|
|
393
|
+
return h('div', {class: c.skipped.length ? 'callout warn' : 'callout', text: `Tiến trình agent để lại: ${parts.join(' · ')}`});
|
|
394
|
+
}
|
|
395
|
+
|
|
364
396
|
function detailView() {
|
|
365
397
|
const d = state.detail;
|
|
366
398
|
if (!d) return h('div', {class: 'card empty'}, state.selected ? 'Đang tải…' : 'Chọn một task để xem nội dung trao đổi.');
|
|
@@ -368,18 +400,25 @@ function detailView() {
|
|
|
368
400
|
const running = task.status === 'running';
|
|
369
401
|
const queued = task.status === 'queued';
|
|
370
402
|
const agentName = task.agent || 'agent';
|
|
403
|
+
const place = task.worktree || task.workspace;
|
|
404
|
+
const metaBits = [folder(task.workspace), agentName,
|
|
405
|
+
task.isolation === 'worktree' ? 'worktree' : 'tuần tự',
|
|
406
|
+
request && 'model' in request && request.model ? request.model : null,
|
|
407
|
+
running ? elapsedEl(task.started_at, 'đã chạy ') : queued ? 'đang xếp hàng' : `mất ${duration(task.duration_ms)}`,
|
|
408
|
+
state.detail?.stats?.usage && 'total_tokens' in state.detail.stats.usage ? `${number(state.detail.stats.usage.total_tokens)} token` : null].filter(Boolean);
|
|
371
409
|
const head = h('div', {class: 'detail-head'},
|
|
372
410
|
h('div', {}, h('h2', {}, 'Task ', h('span', {class: 'mono', text: task.task_id.slice(0, 8)}), pill(task.status)),
|
|
373
|
-
h('div', {class: 'muted mono',
|
|
374
|
-
|
|
411
|
+
h('div', {class: 'meta-line muted mono', title: place},
|
|
412
|
+
metaBits.reduce((acc, value) => acc.length ? [...acc, ' · ', value] : [value], []))),
|
|
375
413
|
running || queued ? h('button', {class: 'btn danger small', onclick: () => cancel(task.task_id), text: 'Hủy task'}) : null);
|
|
376
414
|
const activity = state.detail?.activity;
|
|
377
415
|
const progress = running ? h('div', {class: 'progress', 'aria-label': 'Tiến độ'},
|
|
378
416
|
h('div', {class: 'bar', 'data-bar-start': task.started_at, 'data-bar-total': String((request?.timeout_seconds ?? 300) * 1000), role: 'presentation'}, h('span', {})),
|
|
379
417
|
h('div', {class: 'muted'}, elapsedEl(task.started_at, 'Đã chạy '), ` / giới hạn ${request?.timeout_seconds ?? '—'}s`),
|
|
380
|
-
h('div', {class: 'muted', id: 'activity-line'}, activityText(activity))) : null;
|
|
418
|
+
h('div', {class: 'muted', id: 'activity-line', title: ACTIVITY_HINT}, activityText(activity))) : null;
|
|
381
419
|
const facts = h('dl', {class: 'kv'},
|
|
382
|
-
...[['
|
|
420
|
+
...[['Thư mục làm việc', task.workspace], ['Worktree', task.worktree || '—'],
|
|
421
|
+
['Agent', agentName], ['Cách ly', task.isolation === 'worktree' ? 'worktree (song song)' : 'tuần tự'],
|
|
383
422
|
['Nhánh', task.branch || '—'], ['Quyền tự duyệt', task.autonomy || 'auto'],
|
|
384
423
|
['Gửi lúc', clock(task.queued_at || task.started_at)], ['Bắt đầu chạy', queued ? 'Chưa chạy' : clock(task.started_at)], ['Kết thúc', clock(task.ended_at)],
|
|
385
424
|
['Thời gian chạy', running ? elapsedEl(task.started_at) : queued ? '—' : duration(task.duration_ms)],
|
|
@@ -407,6 +446,7 @@ function detailView() {
|
|
|
407
446
|
task.denied_actions.length ? h('div', {}, h('div', {class: 'msg-head', text: 'Thao tác bị từ chối quyền'}),
|
|
408
447
|
h('div', {class: 'chips'}, task.denied_actions.map(a => h('span', {class: 'chip', text: a.display_name || a.action})))) : null,
|
|
409
448
|
scopeBody(task),
|
|
449
|
+
cleanupBody(task),
|
|
410
450
|
task.error ? h('div', {class: 'callout bad', text: task.error}) : null,
|
|
411
451
|
task.status === 'completed' ? h('div', {class: 'callout warn', text: 'CLI báo hoàn tất nhưng chưa được xác minh. Tech Lead cần xem diff thực tế và chạy test.'}) : null
|
|
412
452
|
];
|
|
@@ -418,10 +458,11 @@ function detailView() {
|
|
|
418
458
|
h('div', {id: 'log-note'}, task[`${state.logTab}_truncated`] ? h('div', {class: 'callout warn', text: 'Log đã bị cắt vì vượt giới hạn.'}) : null),
|
|
419
459
|
h('pre', {class: 'log', id: 'log-pre', text: logText(task)}));
|
|
420
460
|
|
|
421
|
-
return h('article', {class: 'card detail'}, head, progress,
|
|
461
|
+
return h('article', {class: 'card detail'}, head, progress, h('div', {class: 'convo'}, asked, replied),
|
|
422
462
|
changesPanel(task),
|
|
423
463
|
details('steps', `Các bước ${agentName} báo cáo`, h('div', {id: 'steps-body'}, stepsBody(state.detail?.activity))),
|
|
424
464
|
details('logs', `Log của tiến trình ${agentName}`, logs),
|
|
465
|
+
collapsible('more:fact', 'Xem thêm thông số task', facts),
|
|
425
466
|
prompt ? details('prompt', 'Prompt đầy đủ gửi tới agy (đã lọc secret)', h('pre', {class: 'log', text: prompt})) : null);
|
|
426
467
|
}
|
|
427
468
|
|
|
@@ -452,7 +493,7 @@ function modelCard(o) {
|
|
|
452
493
|
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: o.model || 'Mặc định của Antigravity'}),
|
|
453
494
|
h('span', {class: 'muted', text: o.model ? 'Bridge truyền --model này cho mọi task mới.' : 'Bridge không truyền --model; Antigravity tự dùng model đã cấu hình.'})), select),
|
|
454
495
|
loaded?.error ? h('div', {class: 'callout warn', text: `Không lấy được danh sách model: ${loaded.error}`}) : null,
|
|
455
|
-
h('p', {text: 'Áp dụng cho task mới, không cần khởi động lại. Task đang chạy hoặc đang chờ giữ model đã chốt lúc nhận. Danh sách lấy từ `agy models`; model được ghi lại trong từng task.'}));
|
|
496
|
+
more('more:model', h('p', {text: 'Áp dụng cho task mới, không cần khởi động lại. Task đang chạy hoặc đang chờ giữ model đã chốt lúc nhận. Danh sách lấy từ `agy models`; model được ghi lại trong từng task.'})));
|
|
456
497
|
}
|
|
457
498
|
function progressCard(o) {
|
|
458
499
|
const enabled = Boolean(o.progress?.enabled);
|
|
@@ -460,8 +501,10 @@ function progressCard(o) {
|
|
|
460
501
|
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Hiển thị tiến độ từng bước (stream-json)'}),
|
|
461
502
|
h('label', {class: 'perm-row' + (enabled ? ' on' : '')}, toggle,
|
|
462
503
|
h('span', {}, h('strong', {text: enabled ? 'Đang bật' : 'Đang tắt'}),
|
|
463
|
-
h('span', {class: 'muted', text: 'Bridge chạy agy
|
|
464
|
-
|
|
504
|
+
h('span', {class: 'muted', text: 'Bridge chạy agy ở --output-format stream-json; các bước hiện trong chi tiết task. Áp dụng cho task mới.'}))),
|
|
505
|
+
more('more:progress',
|
|
506
|
+
h('p', {text: 'Ngoài kết quả cuối, agy báo các sự kiện init và step_update khi đang làm việc (đã thấy bằng probe-stream trên agy 1.4.x). Các bước là bằng chứng "còn sống" thật cho phần gia hạn tự động.'}),
|
|
507
|
+
h('div', {class: 'callout warn', text: 'Chưa kiểm chứng: nội dung chi tiết bên trong step_update (nên hiển thị là tóm tắt tự động kèm dữ liệu gốc), và việc kết quả cuối ở chế độ này có cùng dạng với chế độ JSON. Nếu task báo lỗi "CLI output is not a valid stream-json final result" hoặc "stream result fields", hãy tắt tùy chọn này để quay về chế độ JSON đã kiểm chứng và gửi tôi thông báo lỗi.'})));
|
|
465
508
|
}
|
|
466
509
|
function budgetCard(o) {
|
|
467
510
|
const budget = o.budget || {task_tokens: null, day_tokens: null, today_tokens: 0};
|
|
@@ -475,7 +518,7 @@ function budgetCard(o) {
|
|
|
475
518
|
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Mức token (đèn cảnh báo)'}),
|
|
476
519
|
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: 'Mỗi task'}), h('span', {class: 'muted', text: 'Task nào dùng nhiều hơn mức này sẽ được đánh dấu "vượt mức" trong danh sách.'})), chooser('task_tokens', BUDGET_TASK_PRESETS, 'Mức token mỗi task')),
|
|
477
520
|
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: 'Mỗi ngày'}), h('span', {class: 'muted', text: `Hôm nay đã dùng ${number(budget.today_tokens)} token qua bridge${budget.over_day ? ' — đã vượt mức.' : '.'}`})), chooser('day_tokens', BUDGET_DAY_PRESETS, 'Mức token mỗi ngày')),
|
|
478
|
-
h('div', {class: 'callout warn', text: 'Đây chỉ là đèn báo, không phải hạn mức thật và không dừng task: agy không cho bridge đọc hạn mức, và số token của một task chỉ biết khi task kết thúc. Số liệu là tổng token (total_tokens) agy báo cho các task chạy qua bridge, tính theo ngày giờ máy của bạn.'}));
|
|
521
|
+
more('more:budget', h('div', {class: 'callout warn', text: 'Đây chỉ là đèn báo, không phải hạn mức thật và không dừng task: agy không cho bridge đọc hạn mức, và số token của một task chỉ biết khi task kết thúc. Số liệu là tổng token (total_tokens) agy báo cho các task chạy qua bridge, tính theo ngày giờ máy của bạn.'})));
|
|
479
522
|
}
|
|
480
523
|
function softTimeoutCard(o) {
|
|
481
524
|
const soft = o.soft_timeout || {enabled: false, limit_seconds: 7200};
|
|
@@ -488,9 +531,11 @@ function softTimeoutCard(o) {
|
|
|
488
531
|
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Gia hạn khi Antigravity còn hoạt động'}),
|
|
489
532
|
h('label', {class: 'perm-row' + (soft.enabled ? ' on' : '')}, toggle,
|
|
490
533
|
h('span', {}, h('strong', {text: soft.enabled ? 'Đang bật' : 'Đang tắt (task dừng đúng hạn)'}),
|
|
491
|
-
h('span', {class: 'muted', text: 'Đến hạn
|
|
534
|
+
h('span', {class: 'muted', text: 'Đến hạn, bridge kiểm tra agy còn làm việc không và gia hạn thay vì dừng ngay.'}))),
|
|
492
535
|
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: 'Giới hạn tuyệt đối'}), h('span', {class: 'muted', text: 'Dù còn hoạt động, task vẫn dừng khi tổng thời gian chạy tới mức này.'})), select),
|
|
493
|
-
|
|
536
|
+
more('more:soft-timeout',
|
|
537
|
+
h('p', {text: 'Cụ thể: khi tới hạn của task, bridge không dừng ngay mà kiểm tra xem agy còn hoạt động không (có byte mới ở output, hoặc file log của agy vừa được ghi). Nếu còn thì gia hạn rồi kiểm tra lại sau 1 phút, rồi 5 phút, rồi mỗi 10 phút; nếu không thấy hoạt động trong khoảng đó thì dừng task. Khi bật, bridge yêu cầu agy ghi log vào .bridge-state/agy-logs.'}),
|
|
538
|
+
h('div', {class: 'callout warn', text: 'Chưa kiểm chứng: tôi chưa biết agy có ghi log trong lúc suy luận ở chế độ --print. Bật rồi giao một task và xem dòng "log agy:" trong chi tiết task: nếu số KB tăng khi agy đang chạy thì tín hiệu này dùng được. Nếu log không tăng, task sẽ dừng đúng hạn như khi tắt tính năng (không tệ hơn).'})));
|
|
494
539
|
}
|
|
495
540
|
function waitCard(o) {
|
|
496
541
|
const wait = o.wait || {seconds: 55, client_tool_timeout_seconds: 115};
|
|
@@ -502,9 +547,10 @@ function waitCard(o) {
|
|
|
502
547
|
const minutes = Math.round(wait.client_tool_timeout_seconds / 60);
|
|
503
548
|
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Thời gian chờ mỗi lần Codex / Claude gọi (tiết kiệm token)'}),
|
|
504
549
|
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: `${wait.seconds} giây`}),
|
|
505
|
-
h('span', {class: 'muted', text: '
|
|
506
|
-
|
|
507
|
-
|
|
550
|
+
h('span', {class: 'muted', text: 'Client bị giữ trong một lệnh duy nhất tối đa mức này; càng dài càng ít lần hỏi lại. Có hiệu lực ngay, không cần khởi động lại.'})), select),
|
|
551
|
+
more('more:wait',
|
|
552
|
+
h('div', {class: 'callout warn'}, `Phải đặt thời hạn tool của client LỚN HƠN mức này, nếu không client sẽ cắt lần chờ và báo timeout. Với mức hiện tại: Codex, trong ~/.codex/config.toml mục [mcp_servers.antigravity] đặt `, h('code', {text: `tool_timeout_sec = ${wait.client_tool_timeout_seconds}`}), ` (khoảng ${minutes} phút), rồi mở lại Codex. Claude Code: biến môi trường MCP_TOOL_TIMEOUT (mili giây) lớn hơn ${wait.seconds * 1000}, theo tài liệu của Claude Code (chưa kiểm chứng ở đây). Lệnh \`agy-mcp-bridge setup codex\` in sẵn dòng này theo mức đang chọn.`),
|
|
553
|
+
h('p', {text: 'Khi giao task, Codex gọi chờ và bị giữ lại trong một lệnh duy nhất cho đến khi Antigravity xong hoặc hết thời gian này. Trong lúc bị giữ, Codex không tốn token. Nếu bạn chưa chắc về thời hạn tool của client, giữ mức 55 giây.'})));
|
|
508
554
|
}
|
|
509
555
|
function effortCard(o) {
|
|
510
556
|
const current = EFFORT_LEVELS.find(level => level.id === (o.effort || 'default')) || EFFORT_LEVELS[0];
|
|
@@ -517,14 +563,21 @@ function effortCard(o) {
|
|
|
517
563
|
}
|
|
518
564
|
function permissionCard(o) {
|
|
519
565
|
const p = o.permission || {level: 'default', controllable: true};
|
|
566
|
+
const current = PERMISSION_LEVELS.find(level => level.id === p.level) || PERMISSION_LEVELS[0];
|
|
567
|
+
const sandbox = h('div', {class: 'callout warn', text: 'Danh sách "Workspace được phép" chỉ kiểm tra đường dẫn của task, không phải sandbox. Với mức Tự động hoặc Toàn quyền, Antigravity có thể tác động ra ngoài thư mục đó.'});
|
|
568
|
+
const risky = p.level === 'auto' || p.level === 'full';
|
|
520
569
|
return h('section', {class: 'card panel wide'}, h('h2', {text: 'Quyền của Antigravity'}),
|
|
521
|
-
h('p', {text: 'Mức quyền Antigravity có khi làm task. Chỉ bạn đổi được ở đây; Codex / Claude không đổi được. Áp dụng cho task mới, không cần khởi động lại. Hạ mức xuống thì áp dụng cả cho task đang chờ; nâng mức lên thì không.'}),
|
|
522
570
|
h('div', {class: 'perm-list', role: 'radiogroup', 'aria-label': 'Quyền của Antigravity'}, PERMISSION_LEVELS.map(level =>
|
|
523
571
|
h('label', {class: `perm-row${level.id === 'full' ? ' danger' : ''}${p.level === level.id ? ' on' : ''}`},
|
|
524
572
|
h('input', {type: 'radio', name: 'perm', value: level.id, checked: p.level === level.id, disabled: !p.controllable, onchange: () => setPermission(level.id)}),
|
|
525
|
-
h('span', {}, h('strong', {text: level.title})
|
|
573
|
+
h('span', {}, h('strong', {text: level.title}))))),
|
|
574
|
+
h('p', {text: current.text}),
|
|
575
|
+
risky ? sandbox : null,
|
|
526
576
|
p.controllable ? null : h('div', {class: 'callout warn', text: `Đang bị khóa bởi biến môi trường BRIDGE_AGY_PERMISSIONS${p.reason && p.reason !== 'BRIDGE_AGY_PERMISSIONS' ? ` (${p.reason})` : ''}. Đổi biến rồi khởi động lại server để mở.`}),
|
|
527
|
-
|
|
577
|
+
more('more:permission',
|
|
578
|
+
h('p', {text: 'Mức quyền Antigravity có khi làm task. Chỉ bạn đổi được ở đây; Codex / Claude không đổi được. Áp dụng cho task mới, không cần khởi động lại. Hạ mức xuống thì áp dụng cả cho task đang chờ; nâng mức lên thì không.'}),
|
|
579
|
+
h('ul', {class: 'list'}, PERMISSION_LEVELS.map(level => h('li', {}, h('strong', {text: `${level.title}: `}), level.text))),
|
|
580
|
+
risky ? null : sandbox));
|
|
528
581
|
}
|
|
529
582
|
function usageView(u) {
|
|
530
583
|
const head = h('div', {class: 'page-head'}, h('div', {}, h('h1', {text: 'Sử dụng'}), h('p', {text: 'Số liệu bridge ghi lại từ các task đã chạy qua nó.'})),
|
|
@@ -541,33 +594,38 @@ function usageView(u) {
|
|
|
541
594
|
const sums = Object.entries(t.usage);
|
|
542
595
|
const usageCard = h('section', {class: 'card panel wide'}, h('h2', {text: 'Usage do agy báo'}),
|
|
543
596
|
sums.length ? [h('dl', {class: 'rows'}, sums.map(([key, value]) => row(usageName(key), number(value)))),
|
|
544
|
-
h('p', {class: 'muted', text: `Cộng dồn từ ${number(t.with_usage)} task có trường usage trong kết quả của agy. Tên trường do agy quyết định; bridge cộng mọi giá trị số.`})]
|
|
597
|
+
more('more:usage-sums', h('p', {class: 'muted', text: `Cộng dồn từ ${number(t.with_usage)} task có trường usage trong kết quả của agy. Tên trường do agy quyết định; bridge cộng mọi giá trị số.`}))]
|
|
545
598
|
: h('p', {class: 'muted', text: 'Chưa có task nào mà kết quả của agy kèm số liệu usage. Với task chạy ở chế độ JSON hoặc stream, nếu agy trả trường usage thì tổng sẽ hiện ở đây.'}));
|
|
546
599
|
const peak = Math.max(1, ...u.by_day.map(d => d.tasks));
|
|
547
|
-
const
|
|
600
|
+
const bar = d => {
|
|
548
601
|
const fill = h('div', {class: 'bar-fill'}); fill.style.width = `${(d.tasks / peak * 100).toFixed(1)}%`;
|
|
549
602
|
return h('div', {class: 'bar-row'}, h('span', {class: 'muted mono', text: `${d.day.slice(8)}/${d.day.slice(5, 7)}`}), h('div', {class: 'bar-track'}, fill),
|
|
550
|
-
h('span', {class: 'mono', text:
|
|
551
|
-
}
|
|
552
|
-
const
|
|
603
|
+
h('span', {class: 'mono', text: `${d.tasks} · ${duration(d.duration_ms)}`}));
|
|
604
|
+
};
|
|
605
|
+
const active = u.by_day.filter(d => d.tasks);
|
|
606
|
+
const dayCard = h('section', {class: 'card panel wide'}, h('h2', {text: '14 ngày gần nhất'}),
|
|
607
|
+
active.length ? h('div', {class: 'bars'}, active.map(bar)) : h('p', {class: 'muted', text: 'Không có task nào trong 14 ngày gần nhất.'}),
|
|
608
|
+
active.length && active.length < u.by_day.length ? h('p', {class: 'muted', text: `${u.by_day.length - active.length} ngày còn lại không có task.`}) : null);
|
|
553
609
|
const groupTable = (title, groups, firstColumn) => h('section', {class: 'card panel wide'}, h('h2', {text: title}),
|
|
554
610
|
groups.length ? h('table', {class: 'usage-table'}, h('thead', {}, h('tr', {}, ...[firstColumn, 'Task', 'Hoàn tất', 'Thời gian', 'Usage'].map(c => h('th', {text: c})))),
|
|
555
|
-
h('tbody', {}, groups.map(g => h('tr', {}, h('td', {class: 'mono', text: g.key}), h('td', {text: number(g.tasks)}), h('td', {text: number(g.completed)}), h('td', {text: duration(g.duration_ms)}),
|
|
611
|
+
h('tbody', {}, groups.map(g => h('tr', {}, h('td', {class: 'mono', text: g.key}), h('td', {text: number(g.tasks)}), h('td', {text: number(g.completed)}), h('td', {text: duration(g.duration_ms)}),
|
|
612
|
+
h('td', {class: 'muted', title: usageLine(g.usage) ?? '', text: usageBrief(g.usage)})))))
|
|
556
613
|
: h('p', {class: 'muted', text: 'Chưa có dữ liệu.'}));
|
|
557
614
|
const recent = h('section', {class: 'card panel wide'}, h('h2', {text: 'Task gần nhất'}),
|
|
558
615
|
u.recent.length ? h('ul', {class: 'path-list'}, u.recent.map(r => h('li', {}, `${clock(r.at)} · ${STATUS[r.status] || r.status} · ${r.model || 'model mặc định'} · effort ${r.effort || 'mặc định'} · ${duration(r.duration_ms)}`,
|
|
559
|
-
|
|
560
|
-
return [head,
|
|
616
|
+
r.usage && Object.keys(r.usage).length ? h('span', {class: 'tag', title: usageLine(r.usage), text: usageBrief(r.usage)}) : null))) : h('p', {class: 'muted', text: 'Chưa có task nào.'}));
|
|
617
|
+
return [head, cards, usageCard, dayCard, groupTable('Theo model', u.by_model, 'Model'), groupTable('Theo mức suy luận', u.by_effort, 'Effort'), recent,
|
|
618
|
+
more('more:usage', callout)];
|
|
561
619
|
}
|
|
562
620
|
function settingsView(o) {
|
|
563
621
|
const delegation = o.delegation;
|
|
564
|
-
return [h('div', {class: 'page-head'}, h('div', {}, h('h1', {text: 'Cài đặt'}), h('p', {text: '
|
|
622
|
+
return [h('div', {class: 'page-head'}, h('div', {}, h('h1', {text: 'Cài đặt'}), h('p', {text: 'Mọi thứ đổi được ở đây áp dụng cho task mới, không cần khởi động lại. Giới hạn cứng vẫn nằm trong config/workspaces.json.'}))),
|
|
565
623
|
h('div', {class: 'settings'},
|
|
566
624
|
h('section', {class: 'card panel wide'}, h('h2', {text: 'Giao việc cho agent'}),
|
|
567
625
|
h('div', {class: 'switch-row'}, h('div', {}, h('strong', {text: delegation.enabled ? 'Đang bật' : 'Đã tắt'}),
|
|
568
626
|
h('span', {class: 'muted', text: delegation.enabled ? 'Codex / Claude có thể giao task mới.' : `Task mới bị từ chối. ${delegation.reason || ''}`})),
|
|
569
627
|
switchControl(delegation.enabled, delegation.controllable, 'Cho phép giao việc cho Antigravity')),
|
|
570
|
-
delegation.controllable ? h('p', {text: 'Tắt để buộc Codex / Claude tự làm. Task đang chạy hoặc đang chờ không bị dừng, nhưng task đang chờ sẽ bị hủy khi đến lượt. Có hiệu lực ngay, không cần khởi động lại.'})
|
|
628
|
+
delegation.controllable ? more('more:delegation', h('p', {text: 'Tắt để buộc Codex / Claude tự làm. Task đang chạy hoặc đang chờ không bị dừng, nhưng task đang chờ sẽ bị hủy khi đến lượt. Có hiệu lực ngay, không cần khởi động lại.'}))
|
|
571
629
|
: h('div', {class: 'callout warn', text: 'Đang bị khóa bởi biến môi trường BRIDGE_DELEGATION. Đổi biến rồi khởi động lại server để mở.'})),
|
|
572
630
|
modelCard(o),
|
|
573
631
|
effortCard(o),
|
|
@@ -578,30 +636,33 @@ function settingsView(o) {
|
|
|
578
636
|
permissionCard(o),
|
|
579
637
|
h('section', {class: 'card panel'}, h('h2', {text: 'Kết nối'}),
|
|
580
638
|
h('div', {class: 'copy-row'}, h('code', {text: o.server.mcp_endpoint}), h('button', {class: 'btn small', onclick: () => copy(o.server.mcp_endpoint), text: 'Sao chép'})),
|
|
581
|
-
h('dl', {class: 'rows'}, row('
|
|
582
|
-
|
|
583
|
-
h('
|
|
639
|
+
h('dl', {class: 'rows'}, row('Phiên bản bridge', o.server.version)),
|
|
640
|
+
h('p', {class: 'callout warn', text: 'Không có xác thực: bất kỳ tiến trình nào chạy trên máy này với cùng user đều có thể điều khiển bridge. Không mở cổng ra ngoài.'}),
|
|
641
|
+
more('more:connect', h('dl', {class: 'rows'}, row('Giao thức', 'MCP Streamable HTTP'), row('Xác thực', 'Không — chỉ 127.0.0.1'), row('Khởi động', clock(o.server.started_at))))),
|
|
584
642
|
h('section', {class: 'card panel wide'}, h('h2', {text: 'Agent CLI'}),
|
|
585
643
|
h('div', {class: 'agent-list'}, (o.agents || []).length ? (o.agents || []).map(agent => h('div', {class: 'switch-row'},
|
|
586
644
|
h('div', {}, h('strong', {text: `${agent.label} · ${agent.id}`}),
|
|
587
|
-
h('span', {class: 'muted', text: [agent.enabled ? 'đang bật' : 'đã tắt', agent.version || 'không rõ phiên bản'
|
|
645
|
+
h('span', {class: 'muted', text: [agent.enabled ? 'đang bật' : 'đã tắt', agent.version || 'không rõ phiên bản'].filter(Boolean).join(' · ')})),
|
|
588
646
|
h('span', {class: `pill ${agent.enabled && agent.available ? 'ok' : agent.enabled ? 'bad' : 'warn'}`, text: !agent.enabled ? 'Tắt' : agent.available ? 'Sẵn sàng' : 'Không có'}))) : h('p', {class: 'muted', text: 'Chưa có agent nào được cấu hình.'})),
|
|
589
647
|
h('p', {class: 'muted', text: `Agent mặc định: ${o.default_agent || 'không có'}. Thứ tự ưu tiên đặt trong config/workspaces.json.`}),
|
|
590
|
-
(o.agents || []).some(agent => (agent.warnings || []).length) ? h('div', {class: 'callout warn', text: (o.agents || []).flatMap(agent => agent.warnings || []).join(' · ')}) : null
|
|
648
|
+
(o.agents || []).some(agent => (agent.warnings || []).length) ? h('div', {class: 'callout warn', text: (o.agents || []).flatMap(agent => agent.warnings || []).join(' · ')}) : null,
|
|
649
|
+
more('more:agents', h('ul', {class: 'list'}, (o.agents || []).map(agent => h('li', {},
|
|
650
|
+
h('strong', {text: `${agent.label}: `}), (agent.capabilities || []).join(', ') || 'không có capability nào'))))),
|
|
591
651
|
h('section', {class: 'card panel'}, h('h2', {text: 'Antigravity CLI'}),
|
|
592
|
-
h('dl', {class: 'rows'}, row('Trạng thái', o.cli.bridge_status), row('Phiên bản', o.cli.version)
|
|
593
|
-
|
|
594
|
-
h('
|
|
652
|
+
h('dl', {class: 'rows'}, row('Trạng thái', o.cli.bridge_status), row('Phiên bản', o.cli.version)),
|
|
653
|
+
h('p', {text: o.cli.authentication}),
|
|
654
|
+
more('more:cli', h('dl', {class: 'rows'}, row('Đường dẫn', o.cli.executable || '—'), row('Cách chạy', o.cli.execution_method || '—')))),
|
|
595
655
|
h('section', {class: 'card panel'}, h('h2', {text: 'Workspace được phép'}),
|
|
596
656
|
o.allowed_workspaces.length ? h('ul', {class: 'path-list'}, o.allowed_workspaces.map(p => h('li', {}, p, (o.session_workspaces || []).includes(p) ? h('span', {class: 'tag', text: 'chỉ lần chạy này'}) : null)))
|
|
597
657
|
: h('div', {class: 'callout warn', text: 'Chưa có workspace nào. Thêm đường dẫn tuyệt đối vào config/workspaces.json rồi khởi động lại.'})),
|
|
598
658
|
h('section', {class: 'card panel'}, h('h2', {text: 'Giới hạn'}),
|
|
599
|
-
h('
|
|
659
|
+
h('p', {text: `${o.limits.concurrent_normal_tasks ?? 1} task thường + ${o.limits.max_parallel_worktrees ?? o.concurrency?.max_parallel_worktrees ?? '—'} worktree cùng lúc, hàng đợi tối đa ${o.limits.max_queued_tasks ?? '—'}, timeout tối đa ${o.limits.max_timeout_seconds ?? '—'}s.`}),
|
|
660
|
+
more('more:limits', h('dl', {class: 'rows'}, row('Timeout tối đa', `${o.limits.max_timeout_seconds ?? '—'}s`), row('Output mỗi luồng', bytes(o.limits.max_output_bytes_per_stream ?? 0)),
|
|
600
661
|
row('Task lưu giữ', o.limits.max_retained_tasks ?? '—'), row('Task thường chạy cùng lúc', o.limits.concurrent_normal_tasks ?? 1),
|
|
601
662
|
row('Worktree chạy song song', o.limits.max_parallel_worktrees ?? o.concurrency?.max_parallel_worktrees ?? '—'),
|
|
602
663
|
row('Quyền tự duyệt mặc định', o.default_autonomy ?? 'auto'),
|
|
603
|
-
row('Hàng đợi tối đa', o.limits.max_queued_tasks ?? '—'))),
|
|
604
|
-
h('section', {class: 'card panel wide'},
|
|
664
|
+
row('Hàng đợi tối đa', o.limits.max_queued_tasks ?? '—')))),
|
|
665
|
+
h('section', {class: 'card panel wide'}, collapsible('more:limitations', 'Lưu ý khi dùng', h('ul', {class: 'list'}, o.limitations.map(item => h('li', {text: item}))))),
|
|
605
666
|
h('section', {class: 'card panel wide'}, h('h2', {text: 'Dừng bridge'}),
|
|
606
667
|
h('p', {text: 'Đóng tab không dừng server. Nút này tắt HTTP server, hủy task đang chạy hoặc đang chờ và dọn các tiến trình agent do bridge khởi tạo. Không thể hoàn tác.'}),
|
|
607
668
|
h('button', {class: 'btn danger', onclick: shutdownBridge, text: 'Tắt bridge'})))];
|
package/docs/HANDOFF.md
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
# Handoff — dọn cây tiến trình agent và thư mục worktree (B + C)
|
|
2
|
-
|
|
3
|
-
Cập nhật: 2026-10-26. Nhánh làm việc: `deepseek` (chưa commit, chưa push, chưa release).
|
|
4
|
-
Người tiếp theo: đọc mục "Chưa làm" trước khi sửa thêm; mọi mục có cách kiểm lại độc lập.
|
|
5
|
-
|
|
6
|
-
## 0. Vấn đề gốc (bằng chứng thật, không suy đoán)
|
|
7
|
-
|
|
8
|
-
Agent CLI không chỉ là một process. `agy` tự khởi động MCP server của nó
|
|
9
|
-
(`cmd /c npx -y open-figma-mcp@latest`, `npx -y chrome-devtools-mcp@latest`) với **thư mục làm việc = worktree**
|
|
10
|
-
được giao việc. Trên Windows, một process giữ thư mục làm việc đang mở thì thư mục đó **không xóa được, không
|
|
11
|
-
đổi tên được**: `git worktree remove`, `rm -rf`, `Remove-Item`, `MoveFile` đều fail (EBUSY / ERROR_ACCESS_DENIED)
|
|
12
|
-
dù thư mục trông trống hoàn toàn. Không log nào, không command line nào của process cho thấy điều đó; cách duy
|
|
13
|
-
nhất để tìm là đọc CWD của process khác qua PEB
|
|
14
|
-
(`OpenProcess(0x0410)` → `NtQueryInformationProcess(class 0)` → PEB+0x20 → +0x38 length, +0x40 buffer) — xem
|
|
15
|
-
`test-results/probe-cwd.ps1`.
|
|
16
|
-
|
|
17
|
-
Chuỗi tiến trình thực đo được từ một lần chạy cầu hình:
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
agy (pid 3300, đã thoát)
|
|
21
|
-
└─ cmd.exe (pid 12848, ĐÃ THOÁT) ← mắt xích chết
|
|
22
|
-
└─ node.exe npx -y open-figma-mcp@latest (pid 29124, còn sống, cwd = worktree)
|
|
23
|
-
└─ cmd.exe /d /s /c open-figma-mcp (pid 4836, còn sống)
|
|
24
|
-
└─ node.exe …\open-figma-mcp\server\dist\index.js (pid 15532, còn sống)
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
`ParentProcessId` của Windows là hằng do process ghi khai khi sinh; nó trỏ vào pid chứ không trỏ vào object, nên
|
|
28
|
-
khi mắt xích giữa chết thì cả nhánh con **vô hình** với mọi query "con của agent". Ba process đó ghim worktree cho
|
|
29
|
-
tới khi bị giết. `taskkill /T` từ gốc 29124 giết cả ba và `rm -rf` thành công ngay sau đó.
|
|
30
|
-
|
|
31
|
-
## 1. Đã làm (code đã build, đã test đơn vị, đã live-test bằng `agy` thật)
|
|
32
|
-
|
|
33
|
-
| # | File | Nội dung |
|
|
34
|
-
|---|------|----------|
|
|
35
|
-
| 1 | `src/isolation/orphans.ts` (mới) | `reapGuard` (PID hợp lệ, không phải bridge, denylist process hệ thống, cửa sổ thời điểm khởi tạo ±5s), `listProcesses` (một query CIM cho toàn bảng), `descendants` (BFS thuần, chống chu cha con), `startTreeWatch` (ghi danh cây tiến trình **trong lúc task chạy**), `reapOwnedTree` (Windows: chỉ giết gốc của từng nhánh còn sống, đối chiếu name+creation để loại PID bị tái sử dụng; POSIX: signal process group của child đã thoát) |
|
|
36
|
-
| 2 | `src/isolation/worktree.ts` | `removeWorktree` trả `{removed, reason}` với retry `[0, 200, 600]` ms thay vì nuốt lỗi; `sweepStaleWorktrees` dọn lúc khởi động: chỉ thư mục **rỗng** mang tên task-id, bỏ lại thư mục còn file, bỏ qua task `queued`/`running`, đọc cả hai shape record |
|
|
37
|
-
| 3 | `src/tasks/manager.ts` | Nối watcher vào vòng đời task (`watchTree` seam, stop khi child đóng, `observed` đưa vào reap); `task.cleanup` là `TreeReport`; giải phóng chạy **sau** khi trạng thái cuối đã công bố và được patch vào record bằng `persist(task, held)`; `REAP_BUDGET_MS = 6000` với timer `unref`; `persist` retry rename khi `EPERM/EACCES/EBUSY` (Windows từ chối rename đè lên record mà UI/client đang mở để đọc); đường launch fail ghi rõ reason khi không xóa được worktree |
|
|
38
|
-
| 4 | `src/runtime.ts` | `watchTree: startTreeWatch` (manager không tự chọn cơ chế OS); gọi `sweepStaleWorktrees` sau `recoverExecutionLock` và log từng thư mục xóa/giữ; hai dòng `limitations` mô tả đúng hành vi mới |
|
|
39
|
-
| 5 | `tests/isolation-orphans.test.ts`, `tests/isolation-worktree.test.ts`, `tests/agents.test.ts`, `tests/hardening-output.test.ts` | Guard rules; giết đúng gốc nhánh; subtree dưới shell đã chết vẫn được giải phóng; PID tái sử dụng không biến cây lạ thành cây của mình; `descendants`; watch poll/accumulate/stop; worktree thật bằng git thật; busy simulation; sweep; thứ tự "status trước, cleanup sau" và request/stats không bị null sau patch; bound snapshot theo `duration_ms` thay vì hằng số 3 |
|
|
40
|
-
|
|
41
|
-
Hành vi không đổi, cố ý giữ: bind `127.0.0.1`, kiểm tra Host/Origin, **không** xác thực token, không cờ bypass
|
|
42
|
-
permission, không tự merge worktree, hard-deny deploy/publish/xóa dữ liệu/push nhánh bảo vệ/đọc credential/thay
|
|
43
|
-
đổi hệ thống ngoài workspace, và **trạng thái cuối chỉ xuất bản sau khi `inspectScope` xong**.
|
|
44
|
-
|
|
45
|
-
## 2. Chưa làm (mỗi dòng là một việc, kèm cách kiểm)
|
|
46
|
-
|
|
47
|
-
1. **Nguyên nhân gốc vẫn còn**: `open-figma-mcp` và `chrome-devtools-mcp` vẫn bật **toàn cục** trong
|
|
48
|
-
`~/.gemini/config/mcp_config.json` (thư mục cấu hình của user chạy bridge). bridge chỉ dọn *sau khi* agent thoát, không ngăn chúng khởi
|
|
49
|
-
động. Chưa tắt bằng `agy mcp disable open-figma-mcp` (lựa chọn A) vì lệnh đó đổi cấu hình cá nhân của người
|
|
50
|
-
dùng và `agy mcp` chỉ có tác dụng global — **không** có `--config-dir`/`--mcp-config` per-task (đã kiểm
|
|
51
|
-
`agy help` ở phiên trước). Hệ quả còn lại: một phiên `agy` chạy *ngoài* bridge vẫn để lại cây ghim thư mục.
|
|
52
|
-
2. **Không có Windows Job Object** (lựa chọn D). Bridge bị giết đột ngột (`taskkill` chính nó, SIGKILL, OS crash,
|
|
53
|
-
mất điện) giữa lúc task chạy thì không có lần giải phóng nào; worktree bị ghim cho tới khi process giữ nó chết.
|
|
54
|
-
`KILL_ON_JOB_CLOSE` mới là bảo đảm thật. Node không có API job, cần native addon hoặc P/Invoke `CreateJobObject`
|
|
55
|
-
+ `AssignProcessToJobObject` lúc spawn — chưa làm, chưa đánh giá chi phí/rủi ro.
|
|
56
|
-
3. **Lỗ hổng của watcher** (có ghi trong mã, chưa bịt): process sinh ra **và** mắt xích cha của nó chết *giữa hai
|
|
57
|
-
nhịp poll* sẽ không bao giờ vào `observed`, nên không thành gốc lúc reap. Nhịp hiện tại `WATCH_DELAYS_MS =
|
|
58
|
-
[500, 1000, 2000, 4000, 10000]` ms (dày lúc `agy` boot MCP server, thưa dần về sau); chưa có bằng chứng cho CLI
|
|
59
|
-
khác (codex, OpenCode…) với mô hình spawn khác — nhịp là suy đoán từ `agy` 1.4.7.
|
|
60
|
-
4. **PID reuse mới chỉ giảm rủi ro, chưa loại bỏ**: cửa sổ ±5s + denylist ảnh hệ thống + đối chiếu name/creation
|
|
61
|
-
giữa record và bảng lúc reap + chạy ngay sau khi child exit. Chưa so image path/handle. Trường hợp xấu nhất vẫn
|
|
62
|
-
là "PID được cấp cho process lạ trùng cửa sổ" — cần quyết định: siết thêm hay chấp nhận và tài liệu hóa.
|
|
63
|
-
5. **Chi phí poll đã đo một lần, chưa tuning và không tắt được**: trên máy dev ngày 2026-10-26, một nhịp
|
|
64
|
-
`Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,Name,CreationDate` (toàn bảng, ~337 dòng) tốn
|
|
65
|
-
**555 ms**; bản lọc `-Filter "ParentProcessId=X"` tốn 172 ms nhưng chỉ tìm được con trực tiếp — không đủ cho mục
|
|
66
|
-
0. Thêm `CommandLine` làm gần gấp đôi chi phí nên bảng không lấy cột đó. Dùng toàn bảng vì một bảng trả được cả
|
|
67
|
-
cây và các cha đã chết. Với nhịp hiện tại, task 300 s ≈ 40 nhịp ≈ 6% chu kỳ làm việc của một `powershell.exe`
|
|
68
|
-
riêng; số đo lấy lúc máy rảnh, chưa đo lúc tải cao hoặc máy chậm, và **chưa có config để tắt watcher hay đổi nhịp**.
|
|
69
|
-
6. **Nhánh group-kill POSIX chưa bao giờ gặp một process group còn sống**: `group_alive`/`signal_group` vẫn chỉ được
|
|
70
|
-
inject trong unit test; trên Linux thật (WSL, Node 20.20.2 — xem mục 3) mock child đã thoát nên `inGroup(pid)` trả
|
|
71
|
-
`false` và release về báo cáo `{checked:true, reaped:[], skipped:[], group_signalled:false}` tức thì — vòng
|
|
72
|
-
`SIGTERM → grace → SIGKILL` chưa chạy lần nào. Chưa có E2E Linux có CLI thật. CI matrix Ubuntu đã chứng minh một
|
|
73
|
-
điều khác và quan trọng hơn: nó bắt được lỗi thật mà Windows không lộ (mục 4).
|
|
74
|
-
7. **Báo cáo giải phóng không có ở UI**: `task.cleanup` tồn tại trong `antigravity_task_status` và
|
|
75
|
-
`.bridge-state/tasks/<id>.json`, nhưng `src/ui` không render (grep `cleanup` trong `src/ui` = 0 kết quả). Nếu
|
|
76
|
-
muốn hiển thị, làm chung với gate 6 trong `docs/MULTI-AGENT-PLAN.md` (UI hiển thị worktree/branch).
|
|
77
|
-
8. **`cleanup` có thể mất**: reap chạy nền sau khi lane đã nhả; nếu server thoát trong ~6 s sau khi agent exit thì
|
|
78
|
-
bản vá record không kịp ghi (chưa có flush-on-shutdown cho reap đang chờ).
|
|
79
|
-
9. **Sweep chỉ xóa thư mục rỗng**: worktree còn file mà không ai review thì còn mãi; branch `bridge/*` không bao giờ
|
|
80
|
-
bị xóa tự động (chính sách "không tự merge/xóa"). Chưa có TTL, chưa có nút/CLI dọn, và **`git worktree prune`
|
|
81
|
-
chưa từng được gọi** → sau khi xóa tay/`sweep`, bản ghi admin trong `<toplevel>/.git/worktrees/*` vẫn còn và
|
|
82
|
-
`git worktree list` báo prunable.
|
|
83
|
-
10. **`removeWorktree` thực tế chỉ chạy ở một nhánh hẹp** (launch chưa kịp spawn). Worktree của task hoàn tất được
|
|
84
|
-
giữ lại có chủ đích, nên đường EBUSY của nó chỉ được chứng minh bằng test mô phỏng, chưa bằng production.
|
|
85
|
-
11. **Chưa commit/push/release**; chưa xin xác nhận của người dùng cho lần thay đổi này (lần chấp nhận trước chỉ
|
|
86
|
-
covering thay đổi đã merge). Scripts kiểm chứng đang ở `test-results/` và **bị gitignore**:
|
|
87
|
-
`verify-reap.mjs` + `probe-cwd.ps1` là đường tái hiện lỗi (đừng xóa); `trace-ancestry.ps1`,
|
|
88
|
-
`list-test-runners.ps1`, `inspect-child.ps1` chỉ là trợ giúp một lần của phiên này — `inspect-child.ps1` còn
|
|
89
|
-
chưa chạy lần nào. Cần quyết: nâng thành `scripts/live-reap.ts` có gate, hay xóa sau khi xong.
|
|
90
|
-
|
|
91
|
-
## 3. Bằng chứng
|
|
92
|
-
|
|
93
|
-
Unit (`npm test` = build + `node dist/scripts/run-tests.js`, 210 test):
|
|
94
|
-
|
|
95
|
-
- Sequential (`node --test --test-concurrency=1 dist/tests/*.test.js`): **207 pass, 0 fail, 3 skipped**, 164,9 s. Đã
|
|
96
|
-
chạy lại lần hai sau bản sửa cuối (nhịp poll thưa hơn) và vẫn 207/0/3 — hai lần độc lập, cùng số.
|
|
97
|
-
- Parallel (`npm test`) trên Windows, chạy liên tiếp 3 lần: `206/1 fail`, `205/2 fail`, `207/0 fail`. Đứng một mình, từng test lỗi đều xanh (progress 9/9 hai lần, bridge.test 27/27).
|
|
98
|
-
- Nhà lỗi luôn là nhóm nhạy cảm timing: `progress` (…signal counts as activity), `http` (startup warns about the local trust model), `soft-timeout` (last activity Ns ago), `hardening-output` (số snapshot). Chúng flake theo tải của runner, không theo code; ba phiên liên tiếp cho ba test khác nhau. Đứng riêng, `progress.test.js` xanh 3/3 lần (9 pass), `bridge.test.js` 27/27. Không nới assertion để né. **Đã chốt ngày 2026-10-27**: `scripts/run-tests.ts` truyền `--test-concurrency=1`, nên từ nay `npm test` — và CI — chạy tuần tự; thời gian trên máy dev Windows là ~158 s thay vì ~47 s. Hai gạch đầu dòng trên là số liệu lịch sử của chế độ song song, không còn tái hiện được bằng `npm test`. Xác nhận sau khi đổi: `npm test` cho **207 pass, 0 fail, 3 skipped** trong 161,7 s (ngày 2026-10-27). Cờ `--test-concurrency` chỉ tồn tại từ Node 20.10 / 18.19 / 21.0 trở đi; CI đặt `node: [20, 22]` nên luôn nhận cờ, còn dev dùng Node 20.0–20.9 sẽ gặp `bad option: --test-concurrency` — cố ý không feature-detect trong script và không nâng `engines.node`, vì đây chỉ là đường chạy test của người phát triển, không phải đường chạy của sản phẩm đã publish.
|
|
99
|
-
- Bộ mới viết xanh tuyệt đối khi chạy riêng: `tests/isolation-orphans.test.js` 9/9, `tests/isolation-worktree.test.js` và `tests/agents.test.js` chạy kèm (`23 pass, 0 fail`).
|
|
100
|
-
- Linux (WSL, Node **20.20.2**, `node --test --test-concurrency=1 dist/tests/*.test.js`) ngày 2026-10-27: **211 test, 210 pass, 0 fail, 1 skipped**, exit 0. Đây là nền mà CI Ubuntu báo đỏ trước đó; chỉ khác Windows ở chỗ nhóm test POSIX signal được chạy thật thay vì bỏ qua.
|
|
101
|
-
- Windows (`npm test`, Node 24.15.0) cùng ngày, sau bản sửa: **211 test, 208 pass, 0 fail, 3 skipped**, 157,8 s. Hai con số lệch nhau đúng 3 = ba test nhóm POSIX signal bỏ qua trên Windows.
|
|
102
|
-
- CI của PR #28 (ubuntu-20/ubuntu-22 **fail**, windows-20/windows-22 pass) hỏng đúng một test `evicted tasks lose their persisted record`, và `publish.yml` trên `main` cũng fail nên **không có bản npm nào được phát hành** (registry vẫn `0.1.19`). Nguyên nhân và cách tái hiện: mục 4; script đo `test-results/diag-evict.mjs`, `test-results/diag-gate.mjs`.
|
|
103
|
-
|
|
104
|
-
Live, `agy` 1.4.7 thật, MCP client `streamable-http`, scratch trong `%TEMP%` (không đụng repo), hai lần:
|
|
105
|
-
|
|
106
|
-
```
|
|
107
|
-
sweep_log : Removed a stale empty worktree directory: …\worktrees\a52a2e42-…
|
|
108
|
-
: Kept worktree directory …\worktrees\0cfa0399-… : it still holds files for review
|
|
109
|
-
task : completed, pid 30288, scope {checked: true, changed: 1, out_of_scope: []}
|
|
110
|
-
cleanup : {checked: true, reaped: [30768, 24304], skipped: [], tracked: 14} (lần 1)
|
|
111
|
-
cleanup : {checked: true, reaped: [33960, 30004, …], skipped: [], tracked: 13} (lần 2, nhịp đã thưa hơn)
|
|
112
|
-
release log : [bridge] Task tree release: 2 process tree(s) reaped, 0 left running
|
|
113
|
-
cwd probe : HITS 0 ← không còn process nào giữ cwd trong scratch
|
|
114
|
-
note.txt : REAP_OK ; original workspace: [.git, a.txt] ← worktree không rò sang repo gốc
|
|
115
|
-
shutdown : POST /ui/api/shutdown → 202, exit code 0
|
|
116
|
-
scratch cleanup : cả hai cây %TEMP% "removed" (không cần force)
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Báo cáo đầy đủ: `test-results/reap-report.json` (gitignored). Chạy lại:
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
npm run build && node test-results/verify-reap.mjs # cần agy đã đăng nhập; tạo scratch trong %TEMP%
|
|
123
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File test-results/probe-cwd.ps1 -Match bridge-reap
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Lưu ý: run **trước** bản sửa cho thấy `HITS 3` đúng với chuỗi tiến trình ở mục 0 (`cleanup.reaped` chỉ có 1 PID)
|
|
127
|
-
— đó là lý do thiết kế chuyển từ "query con trực tiếp lúc exit" sang "ghi cây trong lúc chạy + một bảng tiến trình
|
|
128
|
-
lúc reap".
|
|
129
|
-
|
|
130
|
-
## 4. Cách làm việc cho người tiếp theo
|
|
131
|
-
|
|
132
|
-
- Không tự nới quyền CLI khi gặp permission denial; không thêm cờ bypass; không mở bridge ra LAN/proxy công khai.
|
|
133
|
-
- Bridge cố ý không có token: chỉ dùng trên máy do chủ máy kiểm soát; bất kỳ tiến trình cục bộ cùng user đều điều
|
|
134
|
-
khiển được agent.
|
|
135
|
-
- Trước khi đụng `manager.ts`: giữ nguyên thứ tự `flushOutput → parseResult → inspectScope → release owns lock →
|
|
136
|
-
publish status → persist → appendUsage → release lane → finish → startNext`. Test
|
|
137
|
-
`a worktree task never reports a final status before its owns scope is attached`
|
|
138
|
-
và `a finished task records the agent process trees it released without delaying its status`
|
|
139
|
-
(`tests/agents.test.ts`) chèn cả hai chiều của ràng buộc đó.
|
|
140
|
-
- Test không được phép gọi `powershell.exe`: `TaskManager` chỉ dùng `runtime.watchTree` khi được nối, mặc định inert.
|
|
141
|
-
`createRuntime` (`src/runtime.ts`) là nơi nối `startTreeWatch`.
|
|
142
|
-
- Không xóa `test-results/` trước khi mục 2.11 có quyết định; các file đó là cách tái hiện lỗi.
|
|
143
|
-
- Bản ghi task swap bằng `<id>.json.tmp` rồi `rename`, nên `readdir(state/tasks)` **có thể** trả cả tên `.tmp` khi một
|
|
144
|
-
lần ghi đang chạy. Muốn đếm số bản ghi thì lọc `.json` — `restore()` đã làm đúng như vậy. Đây chính là cách
|
|
145
|
-
`evicted tasks lose their persisted record` hỏng trên Linux mà không hỏng trên Windows.
|
|
146
|
-
- Bản vá muộn sau khi publish status (ghi `task.cleanup`) chỉ được viết khi manager vẫn còn giữ task đó. Retention có
|
|
147
|
-
thể đuổi bản ghi trong lúc query tiến trình chưa về, và ghi tiếp sẽ **hồi sinh** record đã xóa. Test
|
|
148
|
-
`a tree report that lands after eviction patches the record it still owns` (`tests/bridge.test.ts`) dựng đúng cửa sổ
|
|
149
|
-
đó bằng cách cho `reapTree` chờ một gate; đã kiểm chứng là nó đỏ khi bỏ guard, trên cả Windows lẫn Linux.
|
|
150
|
-
- `gh pr merge --auto` merge **ngay lập tức** khi repo không có branch protection: nó đã gộp PR #28 khi 4 job CI còn
|
|
151
|
-
đang chạy. Muốn "gộp khi CI xanh" thì chờ `gh pr checks --watch` rồi merge thủ công, hoặc bật ruleset yêu cầu
|
|
152
|
-
`test (ubuntu/windows × 20/22)` — hiện repo chưa có ruleset nào.
|