@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/docs/command-reference-vietnamese.md +2 -2
  3. package/docs/distribution-standard.md +6 -6
  4. package/docs/operator-manual-vietnamese.md +7 -7
  5. package/docs/publishing-for-teams.md +2 -0
  6. package/docs/release-install-policy.md +2 -2
  7. package/docs/team-onboarding.md +2 -2
  8. package/docs-site/architecture.html +1 -1
  9. package/docs-site/assets/search-en.json +1 -1
  10. package/docs-site/assets/search-vi.json +1 -1
  11. package/docs-site/benchmark.html +1 -1
  12. package/docs-site/commands.html +1 -1
  13. package/docs-site/context-engine.html +1 -1
  14. package/docs-site/ecosystem.html +1 -1
  15. package/docs-site/en/architecture.html +1 -1
  16. package/docs-site/en/benchmark.html +1 -1
  17. package/docs-site/en/commands.html +1 -1
  18. package/docs-site/en/context-engine.html +1 -1
  19. package/docs-site/en/ecosystem.html +1 -1
  20. package/docs-site/en/index.html +15 -15
  21. package/docs-site/en/mcp-approval.html +1 -1
  22. package/docs-site/en/mcp-auth.html +1 -1
  23. package/docs-site/en/mcp-servers.html +1 -1
  24. package/docs-site/en/mcp.html +1 -1
  25. package/docs-site/en/permissions.html +1 -1
  26. package/docs-site/en/profiles.html +1 -1
  27. package/docs-site/en/quickstart.html +1 -1
  28. package/docs-site/en/runtime.html +1 -1
  29. package/docs-site/en/scope.html +1 -1
  30. package/docs-site/en/security.html +1 -1
  31. package/docs-site/en/team.html +1 -1
  32. package/docs-site/en/update.html +3 -3
  33. package/docs-site/en/whats-new.html +48 -27
  34. package/docs-site/en/windows.html +1 -1
  35. package/docs-site/en/workflows.html +1 -1
  36. package/docs-site/index.html +15 -15
  37. package/docs-site/mcp-approval.html +1 -1
  38. package/docs-site/mcp-auth.html +1 -1
  39. package/docs-site/mcp-servers.html +1 -1
  40. package/docs-site/mcp.html +1 -1
  41. package/docs-site/permissions.html +1 -1
  42. package/docs-site/profiles.html +1 -1
  43. package/docs-site/quickstart.html +1 -1
  44. package/docs-site/releases.json +57 -0
  45. package/docs-site/runtime.html +1 -1
  46. package/docs-site/scope.html +1 -1
  47. package/docs-site/security.html +1 -1
  48. package/docs-site/team.html +1 -1
  49. package/docs-site/update.html +3 -3
  50. package/docs-site/whats-new.html +48 -27
  51. package/docs-site/windows.html +1 -1
  52. package/docs-site/workflows.html +1 -1
  53. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  54. package/package.json +1 -1
  55. package/packages/piagent-core/managed/review-snapshot-request.mjs +3 -1
  56. package/packages/piagent-core/managed/runtime-snapshot.mjs +63 -0
  57. package/packages/piagent-core/managed/terminal-boundary.mjs +4 -2
  58. package/packages/piagent-core/managed/tool-boundary.mjs +7 -7
  59. package/packages/piagent-core/managed/ts-diagnostics.mjs +1 -1
  60. package/packages/piagent-core/managed/workflow.mjs +2 -1
  61. package/packages/piagent-core/package.json +1 -1
  62. package/packages/piagent-core/runtime/update/terminal-reload.mjs +23 -13
  63. package/scripts/company-terminal.mjs +10 -5
@@ -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.3" title="v1.48.3 docs · VI"><span class="dot"></span>v1.48.3</a>
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-3" data-release>
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">66</span></summary>
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">
@@ -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.3" title="v1.48.3 docs · VI"><span class="dot"></span>v1.48.3</a>
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>
@@ -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.3" title="v1.48.3 docs · VI"><span class="dot"></span>v1.48.3</a>
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": 643,
260
- "sha256": "1356b4a7b5dce44794b08a4b483941b90da597ed9d7b126c1b875fd58d6e25ff"
259
+ "fileCount": 644,
260
+ "sha256": "789b3bf1850c8e31f08a737575ba3f0c67f45f6f1913c665c350f07f85ed9c20"
261
261
  }
262
262
  ],
263
263
  "custodianComparisonRules": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/platform",
3
- "version": "1.48.3",
3
+ "version": "1.48.5",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -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, worker, mode, ...extra].map(quote).join(' '), timeout: 60 });
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 worker = fileURLToPath(new URL('./terminal-worker.mjs', import.meta.url));
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.runtimeRoot = fs.realpathSync(path.dirname(workerPath));
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);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.48.3",
3
+ "version": "1.48.5",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -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. A run in
6
- // progress is never cut: the footer says the reload waits, and it happens when
7
- // the run settles. How a Terminal reloads is its own: the personal Terminal
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 = Date.now(), pollMs = POLL_MS }) {
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
- if (!idle(ctx)) {
61
- if (!waiting) { waiting = true; ctx.ui?.setStatus?.(STATUS_KEY, `⟳ Nạp Piagent${request.version ? ` ${request.version}` : " mới"} khi xong lượt này`); }
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 moment a run settles is the moment a waiting reload can happen.
80
- pi.on("agent_settled", (event, next) => { keep(event, next); setTimeout(() => { void check(); }, 300).unref?.(); });
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 || heard === session) return;
433
- heard = session;
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
- session.subscribe((event) => {
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'));