@piagent/platform 1.48.3 → 1.48.5
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/CHANGELOG.md +19 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/distribution-standard.md +6 -6
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +2 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/assets/search-en.json +1 -1
- package/docs-site/assets/search-vi.json +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +15 -15
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/update.html +3 -3
- package/docs-site/en/whats-new.html +48 -27
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +15 -15
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/releases.json +57 -0
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/update.html +3 -3
- package/docs-site/whats-new.html +48 -27
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/managed/review-snapshot-request.mjs +3 -1
- package/packages/piagent-core/managed/runtime-snapshot.mjs +63 -0
- package/packages/piagent-core/managed/terminal-boundary.mjs +4 -2
- package/packages/piagent-core/managed/tool-boundary.mjs +7 -7
- package/packages/piagent-core/managed/ts-diagnostics.mjs +1 -1
- package/packages/piagent-core/managed/workflow.mjs +2 -1
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/update/terminal-reload.mjs +23 -13
- package/scripts/company-terminal.mjs +10 -5
package/docs-site/whats-new.html
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
|
|
38
38
|
<span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
|
|
39
39
|
</a>
|
|
40
|
-
<a class="version-badge" href="/whats-new" data-docs-version="v1.48.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.48.5" title="v1.48.5 docs · VI"><span class="dot"></span>v1.48.5</a>
|
|
41
41
|
<nav class="top-nav" aria-label="Tài liệu">
|
|
42
42
|
<a href="/quickstart">Tài liệu</a>
|
|
43
43
|
<a href="/whats-new" aria-current="page">Changelog</a>
|
|
@@ -108,7 +108,30 @@
|
|
|
108
108
|
<div class="release-filter" role="toolbar" aria-label="Lọc theo loại thay đổi"><button type="button" data-filter-kind="all" aria-pressed="true">Tất cả</button><button type="button" data-filter-kind="new" aria-pressed="false">Mới</button><button type="button" data-filter-kind="improved" aria-pressed="false">Cải thiện</button><button type="button" data-filter-kind="fixed" aria-pressed="false">Sửa lỗi</button><button type="button" data-filter-kind="security" aria-pressed="false">Bảo mật</button></div>
|
|
109
109
|
<section class="section releases" data-releases>
|
|
110
110
|
<h2 id="latest" class="sr-only">Mới nhất</h2>
|
|
111
|
-
<article class="release latest" id="v1-48-
|
|
111
|
+
<article class="release latest" id="v1-48-5" data-release>
|
|
112
|
+
<div class="release-meta"><a class="release-version" href="#v1-48-5">v1.48.5</a><time class="release-date" datetime="2026-10-31">31/10/2026</time></div>
|
|
113
|
+
<div class="release-body">
|
|
114
|
+
<h3 data-toc-skip id="v1-48-5-title">Task đang chạy vẫn dùng được tool khi Piagent được cập nhật</h3>
|
|
115
|
+
<p>Cập nhật Piagent khi task công ty đang chạy không còn làm hỏng tool: task chạy xong trên bản đã bắt đầu, rồi Terminal mới nạp bản mới.</p>
|
|
116
|
+
<ul class="release-items">
|
|
117
|
+
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Task công ty chạy trong lúc piagent-update không còn bị mọi tool báo managed-runtime-changed: sandbox chạy bản sao riêng, vẫn được kiểm tra, của đúng bản lúc bắt đầu.</span></li>
|
|
118
|
+
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Lượt không đọc được code không còn bị báo là đã đổi code, trừ khi thật sự có sửa file.</span></li>
|
|
119
|
+
</ul>
|
|
120
|
+
</div>
|
|
121
|
+
</article>
|
|
122
|
+
<article class="release" id="v1-48-4" data-release>
|
|
123
|
+
<div class="release-meta"><a class="release-version" href="#v1-48-4">v1.48.4</a><time class="release-date" datetime="2026-10-30">30/10/2026</time></div>
|
|
124
|
+
<div class="release-body">
|
|
125
|
+
<h3 data-toc-skip id="v1-48-4-title">/reload không còn làm hỏng Terminal công ty; nạp bản mới không làm mất tin nhắn</h3>
|
|
126
|
+
<p>Tool vẫn chạy sau /reload trong piagent studio, bản cập nhật yêu cầu trước /reload vẫn được nạp, và Terminal chờ lúc rảnh hẳn mới nạp bản mới.</p>
|
|
127
|
+
<ul class="release-items">
|
|
128
|
+
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>/reload trong piagent studio không còn làm hỏng cuộc trò chuyện: trước đây tool báo lỗi ctx cũ sau lệnh này.</span></li>
|
|
129
|
+
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Tin nhắn gõ ngay sau khi bấm Esc không còn bị mất: Terminal chỉ nạp bản mới sau 10 giây rảnh liên tục và ô nhập trống.</span></li>
|
|
130
|
+
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Bản cập nhật yêu cầu trước /reload vẫn được nạp, và yêu cầu đã nạp không làm Terminal nạp lại lần nữa.</span></li>
|
|
131
|
+
</ul>
|
|
132
|
+
</div>
|
|
133
|
+
</article>
|
|
134
|
+
<article class="release" id="v1-48-3" data-release>
|
|
112
135
|
<div class="release-meta"><a class="release-version" href="#v1-48-3">v1.48.3</a><time class="release-date" datetime="2026-10-30">30/10/2026</time></div>
|
|
113
136
|
<div class="release-body">
|
|
114
137
|
<h3 data-toc-skip id="v1-48-3-title">Agent gửi thẳng hình và file vào chat; review chạy được trong thư mục nhiều repo</h3>
|
|
@@ -398,36 +421,34 @@
|
|
|
398
421
|
</ul>
|
|
399
422
|
</div>
|
|
400
423
|
</article>
|
|
401
|
-
<article class="release" id="v1-43-3" data-release>
|
|
402
|
-
<div class="release-meta"><a class="release-version" href="#v1-43-3">v1.43.3</a><time class="release-date" datetime="2026-10-26">26/10/2026</time></div>
|
|
403
|
-
<div class="release-body">
|
|
404
|
-
<h3 data-toc-skip id="v1-43-3-title">Dev chọn workflow, harness hỗ trợ BE/FE theo project <span class="tag">không phát hành</span></h3>
|
|
405
|
-
<p>Đã tag nhưng chưa publish: E2E lịch sử WebUI trên Linux phát hiện race giữa scroll và observer. Thay bằng 1.43.4.</p>
|
|
406
|
-
<ul class="release-items">
|
|
407
|
-
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Tự nhận diện repo/package FE, BE trong thư mục hội thoại và gợi ý checks phù hợp, làm mới theo task.</span></li>
|
|
408
|
-
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Context workflow tùy chọn nhớ nguồn requirement, plan và tasks của công việc khi mở lại hội thoại, truyền chỉ đọc cho helper và hỗ trợ đổi hoặc xóa.</span></li>
|
|
409
|
-
<li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Lifecycle là hướng dẫn theo workflow và điểm dừng Dev chọn, không ép file spec hoặc tự tạo gate release. Skill được chọn không cấp quyền hay bỏ qua kiểm chứng, review và duyệt deploy.</span></li>
|
|
410
|
-
</ul>
|
|
411
|
-
</div>
|
|
412
|
-
</article>
|
|
413
|
-
<article class="release" id="v1-43-2" data-release>
|
|
414
|
-
<div class="release-meta"><a class="release-version" href="#v1-43-2">v1.43.2</a><time class="release-date" datetime="2026-10-26">26/10/2026</time></div>
|
|
415
|
-
<div class="release-body">
|
|
416
|
-
<h3 data-toc-skip id="v1-43-2-title">Dev chọn workflow, harness hỗ trợ BE/FE theo project <span class="tag">không phát hành</span></h3>
|
|
417
|
-
<p>Đã tag nhưng chưa publish: wording prompt trùng chuỗi lệnh bị cấm trong test native session. Thay bằng 1.43.4.</p>
|
|
418
|
-
<ul class="release-items">
|
|
419
|
-
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Tự nhận diện repo/package FE, BE trong thư mục hội thoại và gợi ý checks phù hợp, làm mới theo task.</span></li>
|
|
420
|
-
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Context workflow tùy chọn nhớ nguồn requirement, plan và tasks của công việc khi mở lại hội thoại, truyền chỉ đọc cho helper và hỗ trợ đổi hoặc xóa.</span></li>
|
|
421
|
-
<li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Lifecycle là hướng dẫn theo workflow và điểm dừng Dev chọn, không ép file spec hoặc tự tạo gate release. Skill được chọn không cấp quyền hay bỏ qua kiểm chứng, review và duyệt deploy.</span></li>
|
|
422
|
-
</ul>
|
|
423
|
-
</div>
|
|
424
|
-
</article>
|
|
425
424
|
</section>
|
|
426
425
|
<section class="section archive">
|
|
427
426
|
<h2 id="archive">Các bản trước</h2>
|
|
428
427
|
<p class="section-lead">Bản cũ hơn, gom theo tháng. Mở một tháng để xem từng thay đổi.</p>
|
|
429
428
|
<details class="release-archive">
|
|
430
|
-
<summary><span>Tháng 10/2026</span><span class="archive-count">
|
|
429
|
+
<summary><span>Tháng 10/2026</span><span class="archive-count">68</span></summary>
|
|
430
|
+
<div class="archive-row" id="v1-43-3">
|
|
431
|
+
<div class="archive-head"><a class="release-version" href="#v1-43-3">v1.43.3</a> <span class="release-date">26/10/2026</span></div>
|
|
432
|
+
<div class="archive-body">
|
|
433
|
+
<p><strong>Dev chọn workflow, harness hỗ trợ BE/FE theo project</strong> <span class="tag">không phát hành</span> — Đã tag nhưng chưa publish: E2E lịch sử WebUI trên Linux phát hiện race giữa scroll và observer. Thay bằng 1.43.4.</p>
|
|
434
|
+
<ul class="release-items">
|
|
435
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Tự nhận diện repo/package FE, BE trong thư mục hội thoại và gợi ý checks phù hợp, làm mới theo task.</span></li>
|
|
436
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Context workflow tùy chọn nhớ nguồn requirement, plan và tasks của công việc khi mở lại hội thoại, truyền chỉ đọc cho helper và hỗ trợ đổi hoặc xóa.</span></li>
|
|
437
|
+
<li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Lifecycle là hướng dẫn theo workflow và điểm dừng Dev chọn, không ép file spec hoặc tự tạo gate release. Skill được chọn không cấp quyền hay bỏ qua kiểm chứng, review và duyệt deploy.</span></li>
|
|
438
|
+
</ul>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
<div class="archive-row" id="v1-43-2">
|
|
442
|
+
<div class="archive-head"><a class="release-version" href="#v1-43-2">v1.43.2</a> <span class="release-date">26/10/2026</span></div>
|
|
443
|
+
<div class="archive-body">
|
|
444
|
+
<p><strong>Dev chọn workflow, harness hỗ trợ BE/FE theo project</strong> <span class="tag">không phát hành</span> — Đã tag nhưng chưa publish: wording prompt trùng chuỗi lệnh bị cấm trong test native session. Thay bằng 1.43.4.</p>
|
|
445
|
+
<ul class="release-items">
|
|
446
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Tự nhận diện repo/package FE, BE trong thư mục hội thoại và gợi ý checks phù hợp, làm mới theo task.</span></li>
|
|
447
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Context workflow tùy chọn nhớ nguồn requirement, plan và tasks của công việc khi mở lại hội thoại, truyền chỉ đọc cho helper và hỗ trợ đổi hoặc xóa.</span></li>
|
|
448
|
+
<li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Lifecycle là hướng dẫn theo workflow và điểm dừng Dev chọn, không ép file spec hoặc tự tạo gate release. Skill được chọn không cấp quyền hay bỏ qua kiểm chứng, review và duyệt deploy.</span></li>
|
|
449
|
+
</ul>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
431
452
|
<div class="archive-row" id="v1-43-1">
|
|
432
453
|
<div class="archive-head"><a class="release-version" href="#v1-43-1">v1.43.1</a> <span class="release-date">26/10/2026</span></div>
|
|
433
454
|
<div class="archive-body">
|
package/docs-site/windows.html
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
|
|
38
38
|
<span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
|
|
39
39
|
</a>
|
|
40
|
-
<a class="version-badge" href="/whats-new" data-docs-version="v1.48.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.48.5" title="v1.48.5 docs · VI"><span class="dot"></span>v1.48.5</a>
|
|
41
41
|
<nav class="top-nav" aria-label="Tài liệu">
|
|
42
42
|
<a href="/quickstart">Tài liệu</a>
|
|
43
43
|
<a href="/whats-new">Changelog</a>
|
package/docs-site/workflows.html
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
<span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
|
|
38
38
|
<span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
|
|
39
39
|
</a>
|
|
40
|
-
<a class="version-badge" href="/whats-new" data-docs-version="v1.48.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.48.5" title="v1.48.5 docs · VI"><span class="dot"></span>v1.48.5</a>
|
|
41
41
|
<nav class="top-nav" aria-label="Tài liệu">
|
|
42
42
|
<a href="/quickstart">Tài liệu</a>
|
|
43
43
|
<a href="/whats-new">Changelog</a>
|
|
@@ -256,8 +256,8 @@
|
|
|
256
256
|
},
|
|
257
257
|
{
|
|
258
258
|
"path": "tests",
|
|
259
|
-
"fileCount":
|
|
260
|
-
"sha256": "
|
|
259
|
+
"fileCount": 644,
|
|
260
|
+
"sha256": "789b3bf1850c8e31f08a737575ba3f0c67f45f6f1913c665c350f07f85ed9c20"
|
|
261
261
|
}
|
|
262
262
|
],
|
|
263
263
|
"custodianComparisonRules": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
2
|
|
|
3
3
|
const worker = fileURLToPath(new URL('./review-snapshot-worker.mjs', import.meta.url));
|
|
4
|
+
// The session's own copy of the worker (runtime-snapshot.mjs).
|
|
5
|
+
const workerOf = boundary => boundary.runtimeFile?.('review-snapshot-worker.mjs') ?? worker;
|
|
4
6
|
const requests = new WeakSet();
|
|
5
7
|
const quote = value => "'" + value.replaceAll("'", "'\"'\"'") + "'";
|
|
6
8
|
|
|
@@ -9,7 +11,7 @@ const quote = value => "'" + value.replaceAll("'", "'\"'\"'") + "'";
|
|
|
9
11
|
export function invokeReviewSnapshot(boundary, mode, extra = []) {
|
|
10
12
|
if (!['head', 'heads', 'since', 'branch-base', 'digest', 'file', 'files'].includes(mode)
|
|
11
13
|
|| !Array.isArray(extra) || extra.some(value => typeof value !== 'string')) throw Error('review-request-invalid');
|
|
12
|
-
const args = Object.freeze({ command: [process.execPath,
|
|
14
|
+
const args = Object.freeze({ command: [process.execPath, workerOf(boundary), mode, ...extra].map(quote).join(' '), timeout: 60 });
|
|
13
15
|
requests.add(args);
|
|
14
16
|
return boundary.invoke('bash', args);
|
|
15
17
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The code a company session's sandbox runs, kept apart from the install.
|
|
2
|
+
//
|
|
3
|
+
// A session pins the hash of its workers and refuses every tool when one
|
|
4
|
+
// changes. piagent-update rewrites the install in place, so a task running
|
|
5
|
+
// across an update lost all its tools (managed-runtime-changed) while a helper
|
|
6
|
+
// started afterwards worked. A session now runs its workers from a private
|
|
7
|
+
// copy named by their content: an update leaves it alone, the pins keep
|
|
8
|
+
// guarding it, and the next process takes a copy of the new release.
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
13
|
+
|
|
14
|
+
const KEEP_MS = 30 * 24 * 60 * 60 * 1000;
|
|
15
|
+
const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
16
|
+
const cacheRoot = home => path.join(home, process.platform === 'darwin' ? 'Library/Caches/Piagent/runtime' : '.cache/piagent/runtime');
|
|
17
|
+
|
|
18
|
+
function runtimeFiles(source) {
|
|
19
|
+
const files = fs.readdirSync(source, { withFileTypes: true }).filter(entry => entry.isFile() && entry.name.endsWith('.mjs')).map(entry => entry.name);
|
|
20
|
+
const walk = relative => {
|
|
21
|
+
let entries;
|
|
22
|
+
try { entries = fs.readdirSync(path.join(source, relative), { withFileTypes: true }); } catch { return; }
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const name = path.join(relative, entry.name);
|
|
25
|
+
if (entry.isDirectory()) walk(name); else if (entry.isFile()) files.push(name);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
walk(path.join('dist', 'workspace'));
|
|
29
|
+
return files.sort();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const intact = (directory, listing) => listing.every(([file, digest]) => { try { return hash(path.join(directory, file)) === digest; } catch { return false; } });
|
|
33
|
+
|
|
34
|
+
// The copy of `source` under the member's cache folder; an old one is removed
|
|
35
|
+
// once no process has asked for it for a month.
|
|
36
|
+
export function snapshotRuntime(source, { home = os.homedir(), now = Date.now() } = {}) {
|
|
37
|
+
const listing = runtimeFiles(source).map(file => [file, hash(path.join(source, file))]);
|
|
38
|
+
const id = createHash('sha256').update(JSON.stringify(listing)).digest('hex').slice(0, 32);
|
|
39
|
+
const root = cacheRoot(home), target = path.join(root, id);
|
|
40
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
41
|
+
if (!intact(target, listing)) {
|
|
42
|
+
const building = path.join(root, `.${id}.${randomUUID()}`);
|
|
43
|
+
try {
|
|
44
|
+
for (const [file] of listing) {
|
|
45
|
+
fs.mkdirSync(path.dirname(path.join(building, file)), { recursive: true, mode: 0o700 });
|
|
46
|
+
fs.copyFileSync(path.join(source, file), path.join(building, file));
|
|
47
|
+
fs.chmodSync(path.join(building, file), 0o400);
|
|
48
|
+
}
|
|
49
|
+
// Without it the workspace worker's .js files would load as CommonJS.
|
|
50
|
+
fs.writeFileSync(path.join(building, 'package.json'), '{"type":"module"}\n', { mode: 0o400 });
|
|
51
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
52
|
+
// Another session may have built the same copy meanwhile.
|
|
53
|
+
try { fs.renameSync(building, target); } catch (error) { if (!intact(target, listing)) throw error; }
|
|
54
|
+
} finally { fs.rmSync(building, { recursive: true, force: true }); }
|
|
55
|
+
}
|
|
56
|
+
fs.utimesSync(target, new Date(now), new Date(now));
|
|
57
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
58
|
+
if (!entry.isDirectory() || entry.name === id) continue;
|
|
59
|
+
const old = path.join(root, entry.name);
|
|
60
|
+
try { if (now - fs.statSync(old).mtimeMs > KEEP_MS) fs.rmSync(old, { recursive: true, force: true }); } catch { /* another session removes it */ }
|
|
61
|
+
}
|
|
62
|
+
return fs.realpathSync(target);
|
|
63
|
+
}
|
|
@@ -9,18 +9,19 @@ import { companyTerminalRuntime } from './terminal-runtime.mjs';
|
|
|
9
9
|
import { terminalChannel } from './terminal-channel.mjs';
|
|
10
10
|
import { SEATBELT_TERMINAL_RULES, seatbeltTerminal } from './terminal-seatbelt.mjs';
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const installedWorker = fileURLToPath(new URL('./terminal-worker.mjs', import.meta.url));
|
|
13
13
|
const hash = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
14
14
|
const inside = (file, root) => file === root || file.startsWith(root + path.sep);
|
|
15
15
|
const dimensions = ({ cols, rows }) => Number.isInteger(cols) && cols >= 2 && cols <= 500 && Number.isInteger(rows) && rows >= 1 && rows <= 200;
|
|
16
16
|
|
|
17
17
|
export class BoundaryTerminals {
|
|
18
|
-
#boundary; #runtime; #workerDigest; #entries = new Map(); #closed = false;
|
|
18
|
+
#boundary; #runtime; #worker; #workerDigest; #entries = new Map(); #closed = false;
|
|
19
19
|
reasonCode = null;
|
|
20
20
|
constructor(boundary) {
|
|
21
21
|
this.#boundary = boundary;
|
|
22
22
|
try {
|
|
23
23
|
this.#runtime = companyTerminalRuntime();
|
|
24
|
+
const worker = this.#worker = boundary.runtimeFile?.('terminal-worker.mjs') ?? installedWorker; // the session's own copy (runtime-snapshot.mjs)
|
|
24
25
|
if (inside(this.#runtime.root, boundary.cwd) || inside(worker, boundary.cwd)) throw Error('managed-terminal-runtime-overlap');
|
|
25
26
|
this.#workerDigest = hash(worker);
|
|
26
27
|
} catch (error) { this.reasonCode = /^managed-terminal-/.test(error.message) ? error.message : 'managed-terminal-runtime-invalid'; }
|
|
@@ -33,6 +34,7 @@ export class BoundaryTerminals {
|
|
|
33
34
|
if (!dimensions(size)) throw Error('managed-terminal-invalid-size');
|
|
34
35
|
if (this.#entries.size >= 4) throw Error('managed-terminal-limit');
|
|
35
36
|
this.#runtime.assertCurrent();
|
|
37
|
+
const worker = this.#worker;
|
|
36
38
|
if (hash(worker) !== this.#workerDigest) throw Error('managed-terminal-runtime-changed');
|
|
37
39
|
for (const [file, digest] of boundary.pins) if (hash(file) !== digest) throw Error('managed-runtime-changed');
|
|
38
40
|
const temporary = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'piagent-terminal-')));
|
|
@@ -3,7 +3,7 @@ import { SessionMutationObservations } from "./session-mutation-observations.mjs
|
|
|
3
3
|
import { BoundaryTerminals } from './terminal-boundary.mjs';
|
|
4
4
|
import { ToolFreshness } from './tool-freshness.mjs';
|
|
5
5
|
import { BoundaryQueue } from './boundary-queue.mjs';
|
|
6
|
-
import { workspaceWorkerPins, verifyWorkspaceWorker } from './workspace-worker-pins.mjs';
|
|
6
|
+
import { workspaceWorkerPins, verifyWorkspaceWorker } from './workspace-worker-pins.mjs'; import { snapshotRuntime } from './runtime-snapshot.mjs';
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
@@ -202,7 +202,7 @@ export class ManagedToolBoundary {
|
|
|
202
202
|
get sourceControlRevision() { return this.#sourceRequests.revision; }
|
|
203
203
|
source(request, binding) { return this.invoke("workspace", this.#sourceRequests.request(request, binding)); }
|
|
204
204
|
#mutationObservations = new SessionMutationObservations();
|
|
205
|
-
mutationObservation(file) { return this.#mutationObservations.get(file); }
|
|
205
|
+
mutationObservation(file) { return this.#mutationObservations.get(file); } runtimeFile(name) { return path.join(this.runtimeRoot, name); }
|
|
206
206
|
#children = new Set();
|
|
207
207
|
#closed = false;
|
|
208
208
|
// readOnly: the project cannot be written. commands: bash is offered all
|
|
@@ -214,16 +214,16 @@ export class ManagedToolBoundary {
|
|
|
214
214
|
if (process.platform !== 'darwin' && !(this.linux && BWRAP)) throw new Error('managed-sandbox-unavailable');
|
|
215
215
|
this.cwd = fs.realpathSync(cwd); this.sdkRoot = fs.realpathSync(sdkRoot);
|
|
216
216
|
this.node = fs.realpathSync(node);
|
|
217
|
-
this.
|
|
217
|
+
this.sourceRoot = fs.realpathSync(path.dirname(workerPath)); this.runtimeRoot = snapshotRuntime(this.sourceRoot, { home: home ?? os.homedir() }); this.workerPath = this.runtimeFile('tool-worker.mjs'); // an update leaves a running session's workers alone
|
|
218
218
|
this.workspacePins = workspaceWorkerPins(this.runtimeRoot);
|
|
219
219
|
this.sdkPath = path.join(this.sdkRoot, 'dist/index.js');
|
|
220
220
|
this.git = managedGit();
|
|
221
221
|
if (JSON.parse(fs.readFileSync(path.join(this.sdkRoot, 'package.json'), 'utf8')).version !== PI_HOST_VERSION) throw new Error('managed-sdk-version-unqualified');
|
|
222
222
|
const workerModules = ['review-snapshot-worker.mjs', 'tool-freshness.mjs', 'search-fallback.mjs', 'sandbox-diagnostic.mjs', 'ts-diagnostics-server.mjs', 'workspace-worker-pins.mjs'].map(name => path.join(this.runtimeRoot, name));
|
|
223
|
-
this.pins = new Map([this.node, this.git, workerPath, ...workerModules, this.sdkPath].map(file => [file, hash(file)]));
|
|
223
|
+
this.pins = new Map([this.node, this.git, this.workerPath, ...workerModules, this.sdkPath].map(file => [file, hash(file)]));
|
|
224
224
|
const userHome = fs.realpathSync(home ?? os.homedir());
|
|
225
|
-
if ([userHome, '/', this.sdkRoot, this.runtimeRoot].includes(this.cwd)
|
|
226
|
-
|| [this.sdkRoot,this.runtimeRoot,this.node,this.git,...['/opt/homebrew/bin/gh','/usr/local/bin/gh'].filter(p=>fs.existsSync(p)).map(p=>fs.realpathSync(p))].some(p=>inside(p,this.cwd))) throw new Error('managed-project-overlaps-runtime');
|
|
225
|
+
if ([userHome, '/', this.sdkRoot, this.runtimeRoot, this.sourceRoot].includes(this.cwd)
|
|
226
|
+
|| [this.sdkRoot,this.runtimeRoot,this.sourceRoot,this.node,this.git,...['/opt/homebrew/bin/gh','/usr/local/bin/gh'].filter(p=>fs.existsSync(p)).map(p=>fs.realpathSync(p))].some(p=>inside(p,this.cwd))) throw new Error('managed-project-overlaps-runtime');
|
|
227
227
|
this.temporary = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'piagent-tools-')));
|
|
228
228
|
fs.chmodSync(this.temporary, 0o700);
|
|
229
229
|
this.home = path.join(this.temporary, 'home'); fs.mkdirSync(this.home, { mode: 0o700 });
|
|
@@ -371,7 +371,7 @@ export class ManagedToolBoundary {
|
|
|
371
371
|
guard?.();
|
|
372
372
|
const [sandbox, sandboxArgs] = this.linux ? [BWRAP, this.bubblewrap(network, docker)]
|
|
373
373
|
: ['/usr/bin/sandbox-exec', ['-p', docker ? this.dockerProfile : network ? this.networkProfile : isolated ? this.isolatedProfile : this.profile]];
|
|
374
|
-
const child = spawn(sandbox, [...sandboxArgs, this.node, workerPath, this.sdkPath, this.cwd], {
|
|
374
|
+
const child = spawn(sandbox, [...sandboxArgs, this.node, this.workerPath, this.sdkPath, this.cwd], {
|
|
375
375
|
cwd: this.cwd, detached: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
376
376
|
env: docker ? this.environment.docker : network ? this.environment.network : this.environment.offline,
|
|
377
377
|
});
|
|
@@ -21,7 +21,7 @@ export class TsDiagnostics {
|
|
|
21
21
|
if (this.#child) return this.#child;
|
|
22
22
|
if (this.#starts >= RESTARTS) { this.#off = true; return null; }
|
|
23
23
|
this.#starts += 1;
|
|
24
|
-
const { child } = await this.boundary.spawnSandboxed([this.boundary.node, SERVER, this.boundary.cwd], randomUUID(), { stdin: 'pipe' });
|
|
24
|
+
const { child } = await this.boundary.spawnSandboxed([this.boundary.node, this.boundary.runtimeFile?.('ts-diagnostics-server.mjs') ?? SERVER, this.boundary.cwd], randomUUID(), { stdin: 'pipe' });
|
|
25
25
|
child.stdout.on('data', chunk => {
|
|
26
26
|
this.#buffer += chunk.toString('utf8');
|
|
27
27
|
for (let at; (at = this.#buffer.indexOf('\n')) >= 0;) {
|
|
@@ -273,6 +273,7 @@ export class RunProcess {
|
|
|
273
273
|
this.unplannedChange = true; this.planSkipped = true;
|
|
274
274
|
}
|
|
275
275
|
async afterTool(name, args, ok, digestOf) {
|
|
276
|
+
if (['write', 'edit'].includes(name) && ok) this.edited = true;
|
|
276
277
|
if (name === 'bash' && isCheckCommand(args?.command, this.checks.commands)) {
|
|
277
278
|
this.checksRun += 1; if (!ok) this.checksFailed += 1;
|
|
278
279
|
this.lastCheck = { ok, digest: await digestOf(), mutations: this.mutations };
|
|
@@ -334,7 +335,7 @@ export async function completionGate(managed, run, signal) {
|
|
|
334
335
|
let digest = null, interrupted = false, verifyGaveUp = false;
|
|
335
336
|
for (let step = 0; step < 16; step += 1) {
|
|
336
337
|
digest = await digestOf();
|
|
337
|
-
run.changed = run.startDigest !== null && digest !== null ? digest !== run.startDigest : run.mutations > 0;
|
|
338
|
+
run.changed = run.startDigest !== null && digest !== null ? digest !== run.startDigest : run.startDigest !== null ? run.edited === true : run.mutations > 0; // code that cannot be read now: only a real edit says it changed
|
|
338
339
|
// A check that passed still counts when the turn was cut off after it.
|
|
339
340
|
if (run.changed) run.verified = run.verifiedOn(digest);
|
|
340
341
|
run.docsOnly = run.changed && onlyNonCode(managed.ownPaths);
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// `piagent reload` (and `piagent-update` once it installed a release) writes
|
|
4
4
|
// one request file. Each Terminal started before that request takes it when it
|
|
5
|
-
// is idle: no run going, nothing queued, nothing typed in the editor
|
|
6
|
-
// progress is never cut: the footer says the reload
|
|
7
|
-
// the run
|
|
5
|
+
// is idle: no run going, nothing queued, nothing typed in the editor, and quiet
|
|
6
|
+
// for a moment. A run in progress is never cut: the footer says the reload
|
|
7
|
+
// waits, and it happens once the run has settled. How a Terminal reloads is its own: the personal Terminal
|
|
8
8
|
// runs Pi's /reload (Piagent's extensions are read again from disk), the
|
|
9
9
|
// company Terminal starts again in place on the same conversation, since its
|
|
10
10
|
// company code is loaded once per process.
|
|
@@ -14,7 +14,15 @@ import path from "node:path";
|
|
|
14
14
|
|
|
15
15
|
const REQUEST_FILE = "piagent-reload.json";
|
|
16
16
|
const POLL_MS = 2_000;
|
|
17
|
+
// A member who stops a run with Esc usually types the next message right away:
|
|
18
|
+
// a Terminal that starts again in that moment loses what they type.
|
|
19
|
+
const QUIET_MS = 10_000;
|
|
17
20
|
const STATUS_KEY = "piagent-reload";
|
|
21
|
+
// Per process, not per extension runtime: /reload runs this module again, and a
|
|
22
|
+
// request sent before it (the company Terminal's /reload loads no company code)
|
|
23
|
+
// must still be taken, while one already taken must not reload twice.
|
|
24
|
+
const APPLIED_AT = "PIAGENT_RELOAD_APPLIED_AT";
|
|
25
|
+
const processStartedAt = () => Date.now() - process.uptime() * 1000;
|
|
18
26
|
|
|
19
27
|
const requestPath = (home) => path.join(home ?? os.homedir(), ".pi", REQUEST_FILE);
|
|
20
28
|
|
|
@@ -47,8 +55,8 @@ function idle(ctx) {
|
|
|
47
55
|
|
|
48
56
|
// `apply(ctx, request)` reloads this Terminal; it runs at most once per
|
|
49
57
|
// process. Only Terminals with a UI take requests: scripted runs keep theirs.
|
|
50
|
-
export function watchTerminalReload(pi, { apply, home, startedAt =
|
|
51
|
-
let ctx = null, applied = false, waiting = false, timer = null;
|
|
58
|
+
export function watchTerminalReload(pi, { apply, home, startedAt = processStartedAt(), pollMs = POLL_MS, quietMs = QUIET_MS }) {
|
|
59
|
+
let ctx = null, applied = false, waiting = false, timer = null, busyAt = 0;
|
|
52
60
|
// A ctx kept from an earlier session (replaced, reloaded) throws on any use:
|
|
53
61
|
// a check that meets one does nothing, and the next session_start brings a new one.
|
|
54
62
|
const check = async () => {
|
|
@@ -56,17 +64,19 @@ export function watchTerminalReload(pi, { apply, home, startedAt = Date.now(), p
|
|
|
56
64
|
try {
|
|
57
65
|
if (applied || !ctx?.hasUI) return;
|
|
58
66
|
request = readReloadRequest(home);
|
|
59
|
-
if (!request || request.at <= startedAt) return;
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
if (!request || request.at <= Math.max(startedAt, Number(process.env[APPLIED_AT]) || 0)) return;
|
|
68
|
+
const quiet = idle(ctx);
|
|
69
|
+
if (!quiet) busyAt = Date.now();
|
|
70
|
+
if (!quiet || Date.now() - busyAt < quietMs) {
|
|
71
|
+
if (!waiting) { waiting = true; ctx.ui?.setStatus?.(STATUS_KEY, `⟳ Nạp Piagent${request.version ? ` ${request.version}` : " mới"} khi rảnh`); }
|
|
62
72
|
return;
|
|
63
73
|
}
|
|
64
|
-
applied = true;
|
|
74
|
+
applied = true; process.env[APPLIED_AT] = String(request.at);
|
|
65
75
|
ctx.ui?.setStatus?.(STATUS_KEY, undefined);
|
|
66
76
|
} catch { return; }
|
|
67
77
|
try { await apply(ctx, request); }
|
|
68
78
|
catch (error) {
|
|
69
|
-
applied = false;
|
|
79
|
+
applied = false; delete process.env[APPLIED_AT];
|
|
70
80
|
try { ctx.ui?.notify?.(`Chưa nạp được Piagent mới: ${error instanceof Error ? error.message : String(error)}`, "warning"); } catch { /* the session is gone */ }
|
|
71
81
|
}
|
|
72
82
|
};
|
|
@@ -75,9 +85,9 @@ export function watchTerminalReload(pi, { apply, home, startedAt = Date.now(), p
|
|
|
75
85
|
keep(event, next);
|
|
76
86
|
if (!timer) { timer = setInterval(() => { void check(); }, pollMs); timer.unref?.(); }
|
|
77
87
|
});
|
|
78
|
-
pi.on("agent_start", keep);
|
|
79
|
-
// The
|
|
80
|
-
pi.on("agent_settled", (event, next) => { keep(event, next);
|
|
88
|
+
pi.on("agent_start", (event, next) => { keep(event, next); busyAt = Date.now(); });
|
|
89
|
+
// The next check after the quiet time takes a request that waits.
|
|
90
|
+
pi.on("agent_settled", (event, next) => { keep(event, next); busyAt = Date.now(); });
|
|
81
91
|
pi.on("session_shutdown", () => { if (timer) clearInterval(timer); timer = null; ctx = null; });
|
|
82
92
|
return { check };
|
|
83
93
|
}
|
|
@@ -301,6 +301,7 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
|
|
|
301
301
|
let tui;
|
|
302
302
|
const components = async () => (tui ??= await import(pathToFileURL(require.resolve('@earendil-works/pi-tui')).href));
|
|
303
303
|
const installed = (() => { try { return JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version; } catch { return undefined; } })();
|
|
304
|
+
let stopHearing = null;
|
|
304
305
|
|
|
305
306
|
return async (pi) => {
|
|
306
307
|
const { Box, Text } = await components();
|
|
@@ -426,11 +427,12 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
|
|
|
426
427
|
});
|
|
427
428
|
// How the member's message ended (finished, stopped with steps open,
|
|
428
429
|
// failed, stopped), as the dashboard says it under the last answer.
|
|
429
|
-
let heard = null;
|
|
430
430
|
pi.on('session_start', async (_event, ctx) => {
|
|
431
431
|
const session = managed()?.session;
|
|
432
|
-
if (!session?.subscribe
|
|
433
|
-
|
|
432
|
+
if (!session?.subscribe) return;
|
|
433
|
+
// /reload runs this extension again on the same session: the listener of the
|
|
434
|
+
// runtime it replaces goes, or each event reaches a stale ctx and fails the run.
|
|
435
|
+
stopHearing?.();
|
|
434
436
|
// The conversation can close while git answers (a client ends right after a turn): its ctx is then stale.
|
|
435
437
|
const showBranch = async () => { try { const branch = await branchOf(managed()?.cwd ?? ctx.cwd); ctx.ui?.setStatus?.('agent-watch-branch', branch ? `⎇ ${branch}` : undefined); } catch { /* closed meanwhile */ } };
|
|
436
438
|
ctx.ui?.setWorkingMessage?.(WORKING);
|
|
@@ -438,7 +440,7 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
|
|
|
438
440
|
// The checklist and the subagents working now stay above the input (setWidget).
|
|
439
441
|
const showPlan = () => ctx.ui?.setWidget?.('agent-watch-plan', planWidget(currentPlan(session.sessionManager)));
|
|
440
442
|
showPlan();
|
|
441
|
-
|
|
443
|
+
const hear = (event) => {
|
|
442
444
|
if (event?.type === 'tool_execution_end' && event.toolName === 'update_plan' || event?.type === 'agent_end') showPlan();
|
|
443
445
|
if (event?.type === 'managed_helper_state') ctx.ui?.setWidget?.('agent-watch-helpers', helperLines(event.helpers));
|
|
444
446
|
if (event?.type === 'managed_helpers') ctx.ui?.setStatus?.('agent-watch-helpers', event.active ? `Subagent ${event.active}/${event.maximum}` : undefined);
|
|
@@ -448,7 +450,10 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
|
|
|
448
450
|
const copy = turnEndCopy(end, LOCALE), stats = turnStats(session.messages);
|
|
449
451
|
ctx.ui?.notify?.(`${copy.title}. ${copy.text.replace('Bấm Tiếp tục', 'Gửi "tiếp tục"').replace('bấm Tiếp tục', 'gửi "tiếp tục"')}${stats ? `\n${stats}` : ''}`, copy.tone === 'success' ? 'info' : copy.tone === 'error' ? 'error' : copy.tone === 'warning' ? 'warning' : 'info');
|
|
450
452
|
void showBranch();
|
|
451
|
-
}
|
|
453
|
+
};
|
|
454
|
+
// A session closing (a restart in place, /reload) can still report while its ctx is already stale.
|
|
455
|
+
const unsubscribe = session.subscribe((event) => { try { hear(event); } catch { /* the Terminal has moved on */ } });
|
|
456
|
+
stopHearing = unsubscribe;
|
|
452
457
|
});
|
|
453
458
|
pi.registerMessageRenderer('agent-watch-review-status', (_message, _options, theme) =>
|
|
454
459
|
box('Review đã cũ: code đổi sau khi review. Chỉ cần review lại khi đã sửa lỗi blocking.', theme, 'warning'));
|