@piagent/platform 1.50.0 → 1.51.0
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 +23 -0
- package/catalog/capabilities.json +2 -2
- package/docs/command-reference-vietnamese.md +5 -3
- package/docs/company-dev-commands.md +1 -1
- package/docs/distribution-standard.md +6 -6
- package/docs/herdr-workflow.md +30 -0
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +1 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/security-threat-model.md +2 -0
- 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/content/en/workflows.html +6 -0
- package/docs-site/content/workflows.html +6 -0
- 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 +9 -9
- 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 +30 -15
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +7 -1
- package/docs-site/index.html +9 -9
- 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 +51 -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 +30 -15
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +7 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +2 -1
- package/packages/piagent-core/extensions/fallback-base-policy.ts +1 -1
- package/packages/piagent-core/extensions/guard-types.ts +4 -1
- package/packages/piagent-core/{managed/herdr-reporter.mjs → extensions/herdr-reporter.js} +29 -13
- package/packages/piagent-core/extensions/piagent-guard.ts +20 -9
- package/packages/piagent-core/extensions/session-status.d.ts +1 -0
- package/packages/piagent-core/extensions/session-status.js +217 -0
- package/packages/piagent-core/managed/check-questions.mjs +3 -3
- package/packages/piagent-core/managed/command-tools.mjs +46 -7
- package/packages/piagent-core/managed/dist/workspace/assets/packages/piagent-core/policies/base-policy.json +2 -1
- package/packages/piagent-core/managed/dist/workspace/manifest.json +1 -1
- package/packages/piagent-core/managed/fetch-guard.mjs +2 -1
- package/packages/piagent-core/managed/helper-run.mjs +4 -2
- package/packages/piagent-core/managed/host-command.mjs +8 -4
- package/packages/piagent-core/managed/member-mcp.mjs +2 -0
- package/packages/piagent-core/managed/permission.mjs +11 -3
- package/packages/piagent-core/managed/session.mjs +5 -5
- package/packages/piagent-core/managed/tool-boundary.mjs +4 -4
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +2 -1
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +3 -0
- package/packages/piagent-core/runtime/policy/critical-removal.mjs +122 -0
- package/packages/piagent-core/runtime/policy/handoff-policy.mjs +23 -4
- package/packages/piagent-core/runtime/registration/permission-commands.ts +14 -6
- package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +1 -1
- package/packages/piagent-webui/dist/client/assets/{ActivityPanel-CmjxPmNT.js → ActivityPanel-DeeJLOYx.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/App-C9tlGPMb.js +5 -0
- package/packages/piagent-webui/dist/client/assets/{GatewayApp-BmkqeXih.js → GatewayApp-Dvo96qoX.js} +17 -17
- package/packages/piagent-webui/dist/client/assets/{InspectionApp-9jBsgkVJ.js → InspectionApp-CGgHsOyC.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{SessionAgentWorkspace-RQ0N65d9.js → SessionAgentWorkspace-n8UMMcrP.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{SessionInspectorDrawer-BWyEN4bf.js → SessionInspectorDrawer-B62kkr8J.js} +2 -2
- package/packages/piagent-webui/dist/client/assets/SettingsNavigation-BZijmuPt.js +3 -0
- package/packages/piagent-webui/dist/client/assets/SettingsPage-DKg0SfZQ.js +1 -0
- package/packages/piagent-webui/dist/client/assets/{TerminalPanel-DzHdSjpN.js → TerminalPanel-BPTVsNjP.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{WorkspaceCodeEditor-CEuPTTBd.js → WorkspaceCodeEditor-BHGFFBc2.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{WorkspaceExplorer-B_NwYf4S.js → WorkspaceExplorer-nd4kghwK.js} +3 -3
- package/packages/piagent-webui/dist/client/assets/{index-fUpioGMZ.js → index-BiLVX8S-.js} +2 -2
- package/packages/piagent-webui/dist/client/assets/{transcript-view-model-acWtHGyE.js → transcript-view-model-BB1niiXd.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/{view-model-CWNiS4JN.js → view-model-BrdSO09E.js} +1 -1
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/gateway/managed-projection.ts +1 -1
- package/packages/piagent-webui/gateway/runtime-session-controls.ts +1 -1
- package/packages/piagent-webui/gateway/session-command-controller.ts +4 -4
- package/packages/piagent-webui/gateway/session-inspection-registry.ts +1 -1
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +1 -1
- package/schemas/piagent-webui/session-catalog-v1.schema.json +1 -1
- package/schemas/piagent-webui/session-command-v1.schema.json +1 -1
- package/schemas/piagent-webui/snapshot-v1.schema.json +2 -1
- package/schemas/project-profile.schema.json +2 -1
- package/scripts/company-terminal.mjs +8 -7
- package/scripts/pi-auto.sh +5 -1
- package/scripts/piagent-cli.mjs +3 -1
- package/scripts/piagent-sessions.mjs +97 -0
- package/scripts/piagent-studio.mjs +5 -3
- package/packages/piagent-webui/dist/client/assets/App-JrRb7jgn.js +0 -5
- package/packages/piagent-webui/dist/client/assets/SettingsNavigation-Wx-q5QQk.js +0 -3
- package/packages/piagent-webui/dist/client/assets/SettingsPage-xAt6kSyZ.js +0 -1
package/docs-site/team.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.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.51.0" title="v1.51.0 docs · VI"><span class="dot"></span>v1.51.0</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/update.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.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.51.0" title="v1.51.0 docs · VI"><span class="dot"></span>v1.51.0</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>
|
|
@@ -153,9 +153,9 @@
|
|
|
153
153
|
<h2 id="pin-cho-team">Pin một bản cho cả team</h2>
|
|
154
154
|
<p>Khi rollout cho team, cài đúng bản Pi host và đúng bản platform, rồi chạy installer ở chế độ stable. Bước <code>--dry-run</code> in ra những gì sẽ đổi.</p>
|
|
155
155
|
<div class="command-card">
|
|
156
|
-
<header><span>Cài pin theo bản</span><button class="clip-btn" data-clip="npm install -g --ignore-scripts @earendil-works/pi-coding-agent@1.3.0 npm install -g --ignore-scripts @piagent/platform@1.
|
|
156
|
+
<header><span>Cài pin theo bản</span><button class="clip-btn" data-clip="npm install -g --ignore-scripts @earendil-works/pi-coding-agent@1.3.0 npm install -g --ignore-scripts @piagent/platform@1.51.0 piagent-install --stable --dry-run piagent-install --stable">Lấy lệnh</button></header>
|
|
157
157
|
<pre><code>npm install -g --ignore-scripts @earendil-works/pi-coding-agent@1.3.0
|
|
158
|
-
npm install -g --ignore-scripts @piagent/platform@1.
|
|
158
|
+
npm install -g --ignore-scripts @piagent/platform@1.51.0
|
|
159
159
|
piagent-install --stable --dry-run
|
|
160
160
|
piagent-install --stable</code></pre>
|
|
161
161
|
</div>
|
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.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.51.0" title="v1.51.0 docs · VI"><span class="dot"></span>v1.51.0</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,23 @@
|
|
|
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-
|
|
111
|
+
<article class="release latest" id="v1-51-0" data-release>
|
|
112
|
+
<div class="release-meta"><a class="release-version" href="#v1-51-0">v1.51.0</a><time class="release-date" datetime="2026-11-02">02/11/2026</time></div>
|
|
113
|
+
<div class="release-body">
|
|
114
|
+
<h3 data-toc-skip id="v1-51-0-title">Theo dõi mọi session từ bên ngoài, thêm quyền Always bypass</h3>
|
|
115
|
+
<p>Khi mở nhiều session, bạn biết session nào đang chạy, tạm dừng hay bị lỗi mà không cần mở từng cái, từ terminal bất kỳ và ngay trên sidebar Herdr. Always bypass, quyền mới do bạn tự chọn, bỏ sandbox và chỉ hỏi khi commit, push và xoá đường dẫn quan trọng.</p>
|
|
116
|
+
<ul class="release-items">
|
|
117
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>Always bypass: agent và helper không còn sandbox; internet, MCP, file và deploy chạy không hỏi. Chỉ hỏi commit, push, gh pr merge và rm đường dẫn quan trọng. Chỉ cảnh báo một lần.</span></li>
|
|
118
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>piagent sessions liệt kê mọi session đang mở: processing, paused, failed, done hoặc idle, gồm cả cuộc trò chuyện công ty. Thêm --watch để tự làm mới.</span></li>
|
|
119
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>piagent sessions --setup-herdr hiện trạng thái ngay trên từng space và agent ở sidebar Herdr.</span></li>
|
|
120
|
+
<li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Lệnh xoá đường dẫn quan trọng không được trả lời trong 2 phút sẽ không chạy và agent làm tiếp; sau 3 lần, các lệnh này bị từ chối đến khi bạn nhắn tiếp.</span></li>
|
|
121
|
+
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Herdr lại thấy piagent: từ khi Piagent có home riêng, mọi pane đều là unknown và danh sách agent trống.</span></li>
|
|
122
|
+
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Terminal công ty không còn báo idle trên Herdr giữa các vòng của một tin nhắn, và câu hỏi đang chờ bạn được báo là blocked.</span></li>
|
|
123
|
+
<li data-kind="security"><span class="kind kind-security">Bảo mật</span><span>Always bypass không chống được prompt injection: trang web hay file lừa được agent thì secret có thể bị gửi ra ngoài mà không hỏi. Chỉ bật khi bạn chấp nhận điều đó.</span></li>
|
|
124
|
+
</ul>
|
|
125
|
+
</div>
|
|
126
|
+
</article>
|
|
127
|
+
<article class="release" id="v1-50-0" data-release>
|
|
112
128
|
<div class="release-meta"><a class="release-version" href="#v1-50-0">v1.50.0</a><time class="release-date" datetime="2026-11-01">01/11/2026</time></div>
|
|
113
129
|
<div class="release-body">
|
|
114
130
|
<h3 data-toc-skip id="v1-50-0-title">Push được xét theo từng repo và review trước</h3>
|
|
@@ -414,24 +430,23 @@
|
|
|
414
430
|
</ul>
|
|
415
431
|
</div>
|
|
416
432
|
</article>
|
|
417
|
-
<article class="release" id="v1-43-7" data-release>
|
|
418
|
-
<div class="release-meta"><a class="release-version" href="#v1-43-7">v1.43.7</a><time class="release-date" datetime="2026-10-27">27/10/2026</time></div>
|
|
419
|
-
<div class="release-body">
|
|
420
|
-
<h3 data-toc-skip id="v1-43-7-title">Đủ skill đã cài, và mở file agent gửi lại <span class="tag">không phát hành</span></h3>
|
|
421
|
-
<p>Đã tag nhưng chưa publish: kiểm tra wording công khai chặn release vì tên sản phẩm trong ghi chú. Thay bằng 1.43.9.</p>
|
|
422
|
-
<ul class="release-items">
|
|
423
|
-
<li data-kind="new"><span class="kind kind-new">Mới</span><span>File agent gắn link trong câu trả lời mở ngay trong explorer: code đúng dòng, ảnh hiện hình, PDF và file office hiện nội dung chữ.</span></li>
|
|
424
|
-
<li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Skill và lệnh trong plugin Claude và Codex của dev được liệt kê và đọc được, lấy bản mới nhất; gõ /plugin:skill cũng được.</span></li>
|
|
425
|
-
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Chọn thư mục project mở lại được hộp chọn thư mục của Windows, kể cả khi dashboard chạy nền.</span></li>
|
|
426
|
-
</ul>
|
|
427
|
-
</div>
|
|
428
|
-
</article>
|
|
429
433
|
</section>
|
|
430
434
|
<section class="section archive">
|
|
431
435
|
<h2 id="archive">Các bản trước</h2>
|
|
432
436
|
<p class="section-lead">Bản cũ hơn, gom theo tháng. Mở một tháng để xem từng thay đổi.</p>
|
|
433
437
|
<details class="release-archive">
|
|
434
|
-
<summary><span>Tháng 10/2026</span><span class="archive-count">
|
|
438
|
+
<summary><span>Tháng 10/2026</span><span class="archive-count">72</span></summary>
|
|
439
|
+
<div class="archive-row" id="v1-43-7">
|
|
440
|
+
<div class="archive-head"><a class="release-version" href="#v1-43-7">v1.43.7</a> <span class="release-date">27/10/2026</span></div>
|
|
441
|
+
<div class="archive-body">
|
|
442
|
+
<p><strong>Đủ skill đã cài, và mở file agent gửi lại</strong> <span class="tag">không phát hành</span> — Đã tag nhưng chưa publish: kiểm tra wording công khai chặn release vì tên sản phẩm trong ghi chú. Thay bằng 1.43.9.</p>
|
|
443
|
+
<ul class="release-items">
|
|
444
|
+
<li data-kind="new"><span class="kind kind-new">Mới</span><span>File agent gắn link trong câu trả lời mở ngay trong explorer: code đúng dòng, ảnh hiện hình, PDF và file office hiện nội dung chữ.</span></li>
|
|
445
|
+
<li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Skill và lệnh trong plugin Claude và Codex của dev được liệt kê và đọc được, lấy bản mới nhất; gõ /plugin:skill cũng được.</span></li>
|
|
446
|
+
<li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>Chọn thư mục project mở lại được hộp chọn thư mục của Windows, kể cả khi dashboard chạy nền.</span></li>
|
|
447
|
+
</ul>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
435
450
|
<div class="archive-row" id="v1-43-6">
|
|
436
451
|
<div class="archive-head"><a class="release-version" href="#v1-43-6">v1.43.6</a> <span class="release-date">27/10/2026</span></div>
|
|
437
452
|
<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.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.51.0" title="v1.51.0 docs · VI"><span class="dot"></span>v1.51.0</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.
|
|
40
|
+
<a class="version-badge" href="/whats-new" data-docs-version="v1.51.0" title="v1.51.0 docs · VI"><span class="dot"></span>v1.51.0</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>
|
|
@@ -210,6 +210,12 @@ Implement frontend support cho endpoint user status mới. Backend chỉ đọc.
|
|
|
210
210
|
</article>
|
|
211
211
|
</div>
|
|
212
212
|
</section>
|
|
213
|
+
|
|
214
|
+
<section class="section">
|
|
215
|
+
<p class="section-kicker">Nhiều session</p>
|
|
216
|
+
<h2 id="sessions">Xem mọi session mà không cần vào từng space</h2>
|
|
217
|
+
<p class="section-lead"><span class="inline-code">piagent sessions</span> liệt kê mọi session đang mở: <span class="inline-code">processing</span> đang chạy, <span class="inline-code">paused</span> đang chờ bạn, <span class="inline-code">failed</span> bị lỗi, <span class="inline-code">done</span> đã xong. Thêm <span class="inline-code">--watch</span> để tự làm mới. Dùng Herdr thì chạy <span class="inline-code">piagent sessions --setup-herdr</span> một lần để thấy trạng thái ngay trên từng space.</p>
|
|
218
|
+
</section>
|
|
213
219
|
<nav class="pager" aria-label="Trang kế tiếp">
|
|
214
220
|
<a class="pager-prev" href="/commands">
|
|
215
221
|
<span class="pager-label">← Trước</span>
|
|
@@ -256,8 +256,8 @@
|
|
|
256
256
|
},
|
|
257
257
|
{
|
|
258
258
|
"path": "tests",
|
|
259
|
-
"fileCount":
|
|
260
|
-
"sha256": "
|
|
259
|
+
"fileCount": 651,
|
|
260
|
+
"sha256": "02e75d8917cda9a4abc8833a33058700d7ee0baf9553b2efc54d894ff845023b"
|
|
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.
|
|
3
|
+
"version": "1.51.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"bin": {
|
|
73
73
|
"piagent": "scripts/piagent-cli.mjs",
|
|
74
74
|
"piagent-dashboard": "scripts/piagent-cli.mjs",
|
|
75
|
+
"piagent-sessions": "scripts/piagent-cli.mjs",
|
|
75
76
|
"piagent-setup": "scripts/piagent-cli.mjs",
|
|
76
77
|
"piagent-install": "scripts/piagent-cli.mjs",
|
|
77
78
|
"piagent-update": "scripts/piagent-cli.mjs",
|
|
@@ -31,7 +31,7 @@ export function fallbackBasePolicy(
|
|
|
31
31
|
defaultRequiredContext: ["AGENTS.md", "README.md"],
|
|
32
32
|
permissionProfiles: {
|
|
33
33
|
defaultMode: "workspace-write",
|
|
34
|
-
allowedModes: ["read-only", "workspace-write", "trusted-full-access", "bypass"]
|
|
34
|
+
allowedModes: ["read-only", "workspace-write", "trusted-full-access", "bypass", "always-bypass"]
|
|
35
35
|
},
|
|
36
36
|
execPolicy: {
|
|
37
37
|
defaultMode: "enforce",
|
|
@@ -326,7 +326,10 @@ export type ResolvedOrchestrationPolicy = {
|
|
|
326
326
|
// "bypass": personal sessions only; no Piagent gate asks or blocks the work. The
|
|
327
327
|
// handoff (runtime/policy/handoff-policy.mjs) is still asked; output redaction
|
|
328
328
|
// and turn checkpoints still run.
|
|
329
|
-
|
|
329
|
+
// "always-bypass": only committing and pushing code and an rm on a critical path
|
|
330
|
+
// are asked (critical-removal.mjs);
|
|
331
|
+
// the project's autoDeny rules, output redaction and turn checkpoints remain.
|
|
332
|
+
export type PermissionProfileMode = "read-only" | "workspace-write" | "trusted-full-access" | "bypass" | "always-bypass";
|
|
330
333
|
|
|
331
334
|
export type PermissionProfilesConfig = {
|
|
332
335
|
defaultMode?: PermissionProfileMode;
|
|
@@ -7,7 +7,7 @@ import net from 'node:net';
|
|
|
7
7
|
// the company Terminal's own reporter: the same socket requests (newline JSON,
|
|
8
8
|
// pane.report_agent), with the command that reopens this conversation in
|
|
9
9
|
// `piagent studio` instead of personal Pi.
|
|
10
|
-
const
|
|
10
|
+
const COMPANY_SOURCE = 'custom:piagent-studio', AGENT = 'piagent';
|
|
11
11
|
|
|
12
12
|
// What a Herdr pane gives its processes, read before the environment is
|
|
13
13
|
// cleared; null outside Herdr. Nothing else of it reaches the conversation.
|
|
@@ -21,7 +21,7 @@ export function herdrEnvironment(environment = process.env) {
|
|
|
21
21
|
// control characters (its POSIX quoting), so such a path is not offered.
|
|
22
22
|
export function herdrResumeArgv(cwd, sessionFile) {
|
|
23
23
|
const argv = ['piagent', 'studio', '--project', cwd, ...(sessionFile ? ['--session', sessionFile] : [])];
|
|
24
|
-
return argv
|
|
24
|
+
return safeArgv(argv);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// One request, delivered when Herdr answers; a second, longer try when the
|
|
@@ -42,12 +42,15 @@ export async function sendHerdrRequest(socketPath, request) {
|
|
|
42
42
|
if (!await sendOnce(socketPath, request, 500)) await sendOnce(socketPath, request, 1500);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Herdr refuses a resume command with an apostrophe or control characters.
|
|
46
|
+
const safeArgv = argv => argv && argv.every(part => typeof part === 'string' && part && !part.includes("'") && !/\p{Cc}/u.test(part)) ? argv : null;
|
|
47
|
+
|
|
45
48
|
// working while a turn runs, blocked while the conversation waits for the
|
|
46
|
-
// member (Pi's `herdr:blocked` events), idle otherwise.
|
|
47
|
-
// time, the newest state winning.
|
|
48
|
-
export function
|
|
49
|
+
// member (Pi's blocking dialogs, or `herdr:blocked` events), idle otherwise.
|
|
50
|
+
// Reports go out one at a time, the newest state winning.
|
|
51
|
+
export function herdrReporterExtension(herdr, { source, resume, managed = null, send = sendHerdrRequest }) {
|
|
49
52
|
return (pi) => {
|
|
50
|
-
let seq = Date.now() * 1000, active = false, blocked = 0, label, last, resumeArgv, queued, sending = false;
|
|
53
|
+
let unsubscribe, seq = Date.now() * 1000, active = false, blocked = 0, label, last, resumeArgv, queued, sending = false;
|
|
51
54
|
const desired = () => blocked > 0 ? { state: 'blocked', message: label } : { state: active ? 'working' : 'idle', message: undefined };
|
|
52
55
|
const drain = async () => {
|
|
53
56
|
if (sending) return;
|
|
@@ -55,8 +58,8 @@ export function companyHerdrExtension(herdr, { send = sendHerdrRequest } = {}) {
|
|
|
55
58
|
try {
|
|
56
59
|
while (queued) {
|
|
57
60
|
const next = queued; queued = undefined;
|
|
58
|
-
await send(herdr.socket, { id: `${
|
|
59
|
-
params: { pane_id: herdr.pane, source
|
|
61
|
+
await send(herdr.socket, { id: `${source}:${next.seq}`, method: 'pane.report_agent',
|
|
62
|
+
params: { pane_id: herdr.pane, source, agent: AGENT, state: next.state, message: next.message, seq: next.seq, ...(resumeArgv ? { resume_argv: resumeArgv } : {}) } });
|
|
60
63
|
}
|
|
61
64
|
} finally { sending = false; if (queued) void drain(); }
|
|
62
65
|
};
|
|
@@ -68,18 +71,31 @@ export function companyHerdrExtension(herdr, { send = sendHerdrRequest } = {}) {
|
|
|
68
71
|
const follow = ctx => {
|
|
69
72
|
let file;
|
|
70
73
|
try { file = ctx?.sessionManager?.getSessionFile?.(); } catch { file = undefined; }
|
|
71
|
-
const argv =
|
|
74
|
+
const argv = safeArgv(resume(ctx?.cwd, typeof file === 'string' && file ? file : undefined));
|
|
72
75
|
const changed = JSON.stringify(argv) !== JSON.stringify(resumeArgv);
|
|
73
76
|
resumeArgv = argv ?? undefined;
|
|
74
77
|
return changed;
|
|
75
78
|
};
|
|
76
|
-
|
|
79
|
+
const wait = data => {
|
|
77
80
|
if (data?.active) { blocked += 1; label = data.label; } else { blocked = Math.max(0, blocked - 1); if (!blocked) label = undefined; }
|
|
78
81
|
publish();
|
|
79
|
-
}
|
|
82
|
+
};
|
|
83
|
+
pi.events?.on?.('herdr:blocked', wait);
|
|
84
|
+
// A long list as a title is not a label Herdr can show.
|
|
85
|
+
pi.on('ui_prompt_start', event => wait({ active: true, label: event?.title?.slice(0, 80) }));
|
|
86
|
+
pi.on('ui_prompt_end', () => wait({ active: false }));
|
|
80
87
|
// /new, /resume and /fork start another session: Herdr follows it.
|
|
81
|
-
pi.on('session_start', async (_event, ctx) => {
|
|
88
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
89
|
+
follow(ctx); active = ctx?.isIdle?.() === false || !!managed?.()?.activePrompt; publish(true);
|
|
90
|
+
// One company message is many rounds: it is idle when the turn itself ends.
|
|
91
|
+
unsubscribe?.();
|
|
92
|
+
unsubscribe = managed?.()?.session?.subscribe?.(event => { if (event?.type === 'agent_settled' && !managed()?.activePrompt) { active = false; publish(); } });
|
|
93
|
+
});
|
|
82
94
|
pi.on('agent_start', (_event, ctx) => { const changed = follow(ctx); active = true; publish(changed); });
|
|
83
|
-
pi.on('agent_settled', (_event, ctx) => { if (ctx?.isIdle?.() !== true) return; active = false; publish(); });
|
|
95
|
+
pi.on('agent_settled', (_event, ctx) => { if (ctx?.isIdle?.() !== true || managed?.()?.activePrompt) return; active = false; publish(); });
|
|
84
96
|
};
|
|
85
97
|
}
|
|
98
|
+
|
|
99
|
+
export function companyHerdrExtension(herdr, { send, managed } = {}) {
|
|
100
|
+
return herdrReporterExtension(herdr, { source: COMPANY_SOURCE, resume: herdrResumeArgv, managed, ...(send ? { send } : {}) });
|
|
101
|
+
}
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import { detectProfileName } from "./project-shape.js";
|
|
39
39
|
import { evaluateUpdateCheck, isInstalledPlatform, readUpdateCache, startUpdateProbe } from "./update-check.js";
|
|
40
40
|
import { dormantInPersonalPi, personalPiAgentDir } from "./agent-home.js";
|
|
41
|
+
import { registerTerminalSessionStatus } from "./session-status.js";
|
|
41
42
|
import { collectServers } from "../mcp/mcp-config-layers.js";
|
|
42
43
|
import { evaluateMcpApproval, mcpApprovalCache } from "../runtime/mcp-repository-gate.mjs";
|
|
43
44
|
import { evaluateServerReadiness, readinessNotice } from "../mcp/mcp-auth-readiness.js";
|
|
@@ -88,7 +89,8 @@ import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-stat
|
|
|
88
89
|
import { hasDurableContextEvidence } from "./context-evidence.js";
|
|
89
90
|
import { piApprovalBroker, type ApprovalActionDraft, type ApprovalChoice, type ApprovalUnavailableFallback } from "../runtime/inspection/approval-broker.ts";
|
|
90
91
|
import { createSourceMutationGuardBindings } from "../runtime/policy/source-mutation-guard-binding.ts";
|
|
91
|
-
import {
|
|
92
|
+
import { REMOVAL_EXPIRED, REMOVAL_TTL_MS, criticalRemovalReason, noteRemovalExpired, removalsHeld } from "../runtime/policy/critical-removal.mjs";
|
|
93
|
+
import { FULL_ACCESS_RUNS, codeHandoffReason, handoffReason, mcpWriteAction, treeExecFinding } from "../runtime/policy/handoff-policy.mjs";
|
|
92
94
|
import { createWorkspaceWriteAuthorityBindings } from "../runtime/policy/workspace-write-authority.ts";
|
|
93
95
|
import { registerWorkspaceMemberEdits } from "../runtime/session/workspace-member-edits.mjs";
|
|
94
96
|
import {
|
|
@@ -343,6 +345,8 @@ const PERMISSION_PROFILE_ALIASES: Record<string, PermissionProfileMode> = {
|
|
|
343
345
|
"trusted_full_access": "trusted-full-access",
|
|
344
346
|
"danger-full-access": "trusted-full-access",
|
|
345
347
|
"danger_full_access": "trusted-full-access",
|
|
348
|
+
"always-bypass": "always-bypass",
|
|
349
|
+
"always_bypass": "always-bypass",
|
|
346
350
|
bypass: "bypass",
|
|
347
351
|
yolo: "bypass",
|
|
348
352
|
"dangerously-bypass": "bypass",
|
|
@@ -1160,6 +1164,7 @@ function runtimeEquivalentForPermissionProfile(mode: PermissionProfileMode): str
|
|
|
1160
1164
|
// checks are the boundary.
|
|
1161
1165
|
if (mode === "read-only") return "like sandbox_mode=read-only, enforced by Piagent policy (no OS sandbox)";
|
|
1162
1166
|
if (mode === "trusted-full-access") return "like sandbox_mode=danger-full-access; protected paths, destructive steps and the handoff (push, PR, publish, deploy, remote tools) are still asked";
|
|
1167
|
+
if (mode === "always-bypass") return "like sandbox_mode=danger-full-access + approval_policy=never except committing and pushing code and removing a critical path; the project's autoDeny rules, output redaction and turn checkpoints remain";
|
|
1163
1168
|
if (mode === "bypass") return "like sandbox_mode=danger-full-access + approval_policy=never for the work; the handoff (push, PR, publish, deploy, remote tools, MCP writes) is still asked, and output redaction and turn checkpoints remain";
|
|
1164
1169
|
return "like sandbox_mode=workspace-write + approval_policy=on-request, enforced by Piagent policy (no OS sandbox)";
|
|
1165
1170
|
}
|
|
@@ -1239,7 +1244,7 @@ function isPiagentTool(toolName: string): boolean {
|
|
|
1239
1244
|
}
|
|
1240
1245
|
|
|
1241
1246
|
// Full access or more: the profile does not narrow reads or the tool registry.
|
|
1242
|
-
const broadAccess = (mode: PermissionProfileMode) => mode === "trusted-full-access" || mode === "bypass";
|
|
1247
|
+
const broadAccess = (mode: PermissionProfileMode) => mode === "trusted-full-access" || mode === "bypass" || mode === "always-bypass";
|
|
1243
1248
|
|
|
1244
1249
|
function evaluatePermissionProfileToolAccess(
|
|
1245
1250
|
toolName: string,
|
|
@@ -3346,6 +3351,7 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
3346
3351
|
// The member's own `pi` stays stock even when an older install or a project
|
|
3347
3352
|
// still registers this package there; Piagent runs through `piagent`.
|
|
3348
3353
|
if (dormantInPersonalPi()) return;
|
|
3354
|
+
registerTerminalSessionStatus(pi);
|
|
3349
3355
|
const extensionDir = path.dirname(fileURLToPath(import.meta.url));
|
|
3350
3356
|
const policy = loadPolicy(extensionDir);
|
|
3351
3357
|
const bashResults = createBashResultLedger({ maxEntries: 300 });
|
|
@@ -3754,8 +3760,9 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
3754
3760
|
});
|
|
3755
3761
|
|
|
3756
3762
|
// Bypass is for personal sessions only: a company session keeps Studio's rules.
|
|
3757
|
-
const
|
|
3758
|
-
|
|
3763
|
+
const bypassMode = (ctx: ExtensionContext) => !isCompanySession(ctx) && ctx.model?.provider !== "agent_watch_managed"
|
|
3764
|
+
? resolvePermissionProfile(loadProfileFromContext(ctx), policy, permissionOverrideFromContext(ctx)).mode : null;
|
|
3765
|
+
const bypassActive = (ctx: ExtensionContext) => ["bypass", "always-bypass"].includes(String(bypassMode(ctx)));
|
|
3759
3766
|
// Bypass runs the work without asking and stops at the handoff: a push, a
|
|
3760
3767
|
// PR or merge, a publish or deploy, a remote or cloud tool, an upload or an
|
|
3761
3768
|
// MCP tool that writes outside is asked about, as in every other mode.
|
|
@@ -3770,8 +3777,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
3770
3777
|
const refused = evaluateExecPolicyCore(command, { policy: { execPolicy: { defaultMode: "enforce", bannedPrefixSuggestions: [], rules: projectRules } }, mode: "enforce" });
|
|
3771
3778
|
if (refused.decision === "forbid") return execRefusal(refused);
|
|
3772
3779
|
}
|
|
3773
|
-
|
|
3774
|
-
|
|
3780
|
+
const always = bypassMode(ctx) === "always-bypass", modeName = always ? "Always bypass" : "Bypass";
|
|
3781
|
+
const removal = always && command ? criticalRemovalReason(command, { cwd: ctx.cwd, home: os.homedir() }) : null;
|
|
3782
|
+
let reason = !command ? null : always ? removal ?? codeHandoffReason(command) : await handoffReason(command);
|
|
3783
|
+
if (removal && removalsHeld(ctx.sessionManager.getSessionId(), ctx.sessionManager)) return { block: true, reason: `${removal}. ${REMOVAL_EXPIRED}` };
|
|
3784
|
+
if (!always && !reason && !command && prepared && classifyExternalAction(event.toolName, prepared.input, policy).decision === "confirm") {
|
|
3775
3785
|
const action = await mcpWriteAction(prepared.proxyTool ?? event.toolName);
|
|
3776
3786
|
if (action) reason = `${prepared.proxyToolName ?? event.toolName} writes outside this machine (${action})`;
|
|
3777
3787
|
}
|
|
@@ -3780,10 +3790,11 @@ export default function piagentGuard(pi: ExtensionAPI) {
|
|
|
3780
3790
|
const approval = await piApprovalBroker.request({ cwd: ctx.cwd, rawSessionId: ctx.sessionManager.getSessionId(), toolCallId: String(event.toolCallId), expectedTask: null,
|
|
3781
3791
|
action: { kind: "external-provider-action", preconditionClass: "runtime-only", treePrecondition: null, toolName: event.toolName, rawAction: { toolName: event.toolName, command, input },
|
|
3782
3792
|
commandPreview: command, parameterPreview: preview, targetPaths: [], targetSummaries: [reason], provider: command ? "shell-external" : String(prepared?.proxyToolName ?? event.toolName),
|
|
3783
|
-
urlOrigin: null, requestedScope: "one-handoff", reason:
|
|
3793
|
+
urlOrigin: null, requestedScope: "one-handoff", reason: `${modeName} still asks before a handoff: ${reason}`, riskClass: "high",
|
|
3784
3794
|
allowConsequence: "Run this exact step once; the work leaves this machine.", denyConsequence: "The step does not run; the agent is told you declined." },
|
|
3785
|
-
terminalConfirm: () => ctx.ui.confirm(
|
|
3786
|
-
terminalChoose: terminalChooser(ctx, "Piagent handoff confirmation",
|
|
3795
|
+
terminalConfirm: () => ctx.ui.confirm(`${modeName} still asks before a handoff.\n\n${reason}\n\n${preview}\n\nAllow?`, "Piagent handoff confirmation"),
|
|
3796
|
+
terminalChoose: terminalChooser(ctx, "Piagent handoff confirmation", `${modeName} still asks before a handoff.\n\n${reason}\n\n${preview}`, false), unavailableFallback: approvalUnavailableFallback(ctx), ...(removal ? { ttlMs: REMOVAL_TTL_MS } : {}) });
|
|
3797
|
+
if (removal && approval.receipt?.state === "expired") { noteRemovalExpired(ctx.sessionManager.getSessionId(), ctx.sessionManager); return { block: true, reason: `${removal}. ${REMOVAL_EXPIRED}` }; }
|
|
3787
3798
|
if (!approval.allowed) return approval.choice === "stop" ? { block: true, terminate: true, reason: `User denied the handoff and stopped the turn: ${reason}. Do not retry; wait for the member.` } : { block: true, reason: `User denied the handoff: ${reason}` };
|
|
3788
3799
|
if (!approval.consume()) return { block: true, reason: "Approval became stale before tool start; the action was blocked." };
|
|
3789
3800
|
return undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function registerTerminalSessionStatus(pi: unknown, env?: NodeJS.ProcessEnv): void;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { piagentAgentDir, agentWatchControlDir } from "./agent-home.js";
|
|
4
|
+
import { herdrEnvironment, herdrReporterExtension, sendHerdrRequest } from "./herdr-reporter.js";
|
|
5
|
+
|
|
6
|
+
// What every open Piagent session is doing, readable from outside it: one small
|
|
7
|
+
// file per Terminal under <piagent home>/status/sessions/, listed by
|
|
8
|
+
// `piagent sessions`, and inside Herdr a summary on the row of each space, so a
|
|
9
|
+
// developer with many sessions sees which one runs, waits or failed without
|
|
10
|
+
// opening it. A file never holds the member's prompt or the agent's answer.
|
|
11
|
+
export const SESSION_STATES = ["failed", "paused", "processing", "done", "idle"];
|
|
12
|
+
const GLYPHS = { failed: "✖", paused: "⏸", processing: "▶", done: "✓" };
|
|
13
|
+
const HEARTBEAT_MS = 30_000, DEAD_AFTER_MS = 120_000, COMPANY_KEEP_MS = 24 * 3_600_000;
|
|
14
|
+
const SPACE_SOURCE = "piagent:sessions", PANE_SOURCE = "piagent:session";
|
|
15
|
+
export const SPACE_TOKEN = "piagent_sessions", PANE_TOKEN = "piagent_status";
|
|
16
|
+
|
|
17
|
+
const clip = (text, n) => (text.length > n ? `${text.slice(0, n - 1)}…` : text);
|
|
18
|
+
export const sessionStatusDir = (agentDir = piagentAgentDir()) => path.join(agentDir, "status", "sessions");
|
|
19
|
+
|
|
20
|
+
function writeAtomic(file, value) {
|
|
21
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
22
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
23
|
+
fs.writeFileSync(temporary, JSON.stringify(value), { mode: 0o600 });
|
|
24
|
+
fs.renameSync(temporary, file);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readJsonFiles(dir) {
|
|
28
|
+
let names;
|
|
29
|
+
try { names = fs.readdirSync(dir); } catch { return []; }
|
|
30
|
+
return names.filter(name => name.endsWith(".json")).flatMap(name => {
|
|
31
|
+
try { return [{ file: path.join(dir, name), value: JSON.parse(fs.readFileSync(path.join(dir, name), "utf8")) }]; } catch { return []; }
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function processAlive(pid) {
|
|
36
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error?.code === "EPERM"; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The last turn's outcome: Pi's retries end in one agent_end per attempt, so
|
|
40
|
+
// only what the final one says counts.
|
|
41
|
+
export function failureOf(messages) {
|
|
42
|
+
const last = [...(messages ?? [])].reverse().find(message => message?.role === "assistant");
|
|
43
|
+
return last?.stopReason === "error" ? clip(String(last.errorMessage || "The model request failed"), 160) : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A Terminal that vanished in the middle of a turn failed; one that vanished
|
|
47
|
+
// at rest has nothing left to say. The heartbeat tells a reused pid apart.
|
|
48
|
+
export function liveRow(value, now, alive = processAlive) {
|
|
49
|
+
if (alive(value.pid) && now - Date.parse(value.updated_at) <= DEAD_AFTER_MS) return { ...value, alive: true };
|
|
50
|
+
if (value.state === "processing" || value.state === "paused") return { ...value, alive: false, state: "failed", message: "The Terminal ended during the task" };
|
|
51
|
+
return value.state === "failed" ? { ...value, alive: false } : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Conversations of the dashboard and of Agent Watch's company Gateway publish
|
|
55
|
+
// their own files (packages/piagent-webui/gateway/agent-status-files.ts).
|
|
56
|
+
export function gatewayRow(value, source) {
|
|
57
|
+
const state = value.state === "working" ? "processing" : value.state === "blocked" ? "paused"
|
|
58
|
+
: value.state === "error" || value.ended === "failed" ? "failed"
|
|
59
|
+
: value.ended === "stopped" ? "paused" : value.state === "done" ? "done" : "idle";
|
|
60
|
+
return { id: `${source}:${value.conversation}`, source, state, project: value.project, title: value.title, since: value.since, updated_at: value.updated_at,
|
|
61
|
+
...(value.state === "blocked" && value.kind ? { message: `Waiting: ${value.kind}` } : {}) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function readSessions({ agentDir = piagentAgentDir(), controlDir = agentWatchControlDir(), now = Date.now(), alive = processAlive, prune = false, gateways = true } = {}) {
|
|
65
|
+
const rows = [];
|
|
66
|
+
for (const { file, value } of readJsonFiles(sessionStatusDir(agentDir))) {
|
|
67
|
+
const row = liveRow(value, now, alive);
|
|
68
|
+
if (row && (row.alive || now - Date.parse(row.updated_at) <= COMPANY_KEEP_MS)) rows.push(row);
|
|
69
|
+
else if (prune) fs.rmSync(file, { force: true });
|
|
70
|
+
}
|
|
71
|
+
for (const source of gateways ? ["company", "dashboard"] : []) {
|
|
72
|
+
for (const { value } of readJsonFiles(path.join(controlDir, "status", source))) {
|
|
73
|
+
if (now - Date.parse(value.updated_at) <= COMPANY_KEEP_MS) rows.push(gatewayRow(value, source));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return rows.sort((a, b) => SESSION_STATES.indexOf(a.state) - SESSION_STATES.indexOf(b.state) || String(b.since).localeCompare(String(a.since)));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// One session in a space reads as a word, several as counts; what needs the
|
|
80
|
+
// developer comes first. Nothing to show clears the token.
|
|
81
|
+
export function spaceSummary(rows) {
|
|
82
|
+
const shown = rows.filter(row => GLYPHS[row.state]);
|
|
83
|
+
if (!shown.length) return null;
|
|
84
|
+
if (shown.length === 1) return `${GLYPHS[shown[0].state]} ${shown[0].state}`;
|
|
85
|
+
return SESSION_STATES.filter(state => GLYPHS[state]).flatMap(state => {
|
|
86
|
+
const count = shown.filter(row => row.state === state).length;
|
|
87
|
+
return count ? [`${GLYPHS[state]}${count}`] : [];
|
|
88
|
+
}).join(" ");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// How a company message ended (managed/continuation.mjs). A checklist left
|
|
92
|
+
// open waits for the member to say what comes next.
|
|
93
|
+
export function companyTurnEnd(end, text) {
|
|
94
|
+
if (end?.state === "failed") return ["failed", clip(`${end.role} agent: ${end.code}`, 160)];
|
|
95
|
+
if (end?.state === "cancelled") return ["paused", "Stopped by the member"];
|
|
96
|
+
if (end?.state === "midway") return ["paused", clip(String(text || "The checklist is not finished"), 160)];
|
|
97
|
+
return ["done", undefined];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Only one session of a process is current: a company Terminal that enrolled
|
|
101
|
+
// again replaces its session without always ending the old one.
|
|
102
|
+
let currentInstance = null;
|
|
103
|
+
|
|
104
|
+
export function sessionStatusExtension({ agentDir = piagentAgentDir(), herdr = null, send = sendHerdrRequest, now = Date.now, pid = process.pid, heartbeatMs = HEARTBEAT_MS, managed = null } = {}) {
|
|
105
|
+
return (pi) => {
|
|
106
|
+
const instance = {}, file = path.join(sessionStatusDir(agentDir), `${pid}.json`);
|
|
107
|
+
const space = herdr ? herdr.pane.split(":")[0] : null;
|
|
108
|
+
let started = false, state = "idle", message, since = new Date(now()).toISOString(), cwd, sessionId, prompts = 0, failure = null, settled = "idle", settledNote, unsubscribe, timer, sequence = now() * 1000;
|
|
109
|
+
|
|
110
|
+
const current = () => started && currentInstance === instance;
|
|
111
|
+
const nextSeq = () => { sequence = Math.max(sequence + 1, now() * 1000); return sequence; };
|
|
112
|
+
const spaceRequest = (workspaceId, summary) => ({ id: `${SPACE_SOURCE}:${nextSeq()}`, method: "workspace.report_metadata",
|
|
113
|
+
params: { workspace_id: workspaceId, source: SPACE_SOURCE, seq: sequence, ttl_ms: DEAD_AFTER_MS, tokens: { [SPACE_TOKEN]: summary } } });
|
|
114
|
+
// Every space of this Herdr, so a live session also corrects the row of one
|
|
115
|
+
// whose Terminal died. Another Herdr server numbers its spaces on its own.
|
|
116
|
+
const reportSpaces = async () => {
|
|
117
|
+
if (!herdr) return;
|
|
118
|
+
const rows = readSessions({ agentDir, now: now(), gateways: false }).filter(row => row.socket === herdr.socket);
|
|
119
|
+
for (const id of new Set([space, ...rows.map(row => row.workspace)])) {
|
|
120
|
+
await send(herdr.socket, spaceRequest(id, spaceSummary(rows.filter(row => row.workspace === id))));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const report = async () => {
|
|
124
|
+
if (!herdr) return;
|
|
125
|
+
await send(herdr.socket, { id: `${PANE_SOURCE}:${nextSeq()}`, method: "pane.report_metadata",
|
|
126
|
+
params: { pane_id: herdr.pane, source: PANE_SOURCE, seq: sequence, ttl_ms: DEAD_AFTER_MS, tokens: { [PANE_TOKEN]: message ? `${state}: ${clip(message, 40)}` : state } } });
|
|
127
|
+
await reportSpaces();
|
|
128
|
+
};
|
|
129
|
+
const write = () => {
|
|
130
|
+
try {
|
|
131
|
+
const name = pi.getSessionName?.();
|
|
132
|
+
writeAtomic(file, { schema_version: 1, id: sessionId ?? String(pid), source: "terminal", pid, state, ...(message ? { message } : {}),
|
|
133
|
+
project: cwd ? path.basename(cwd) : "", cwd, ...(name ? { title: clip(name, 80) } : {}),
|
|
134
|
+
...(herdr ? { workspace: space, pane: herdr.pane, socket: herdr.socket } : {}), since, updated_at: new Date(now()).toISOString() });
|
|
135
|
+
} catch { /* the conversation never depends on this view */ }
|
|
136
|
+
};
|
|
137
|
+
const set = (next, note) => {
|
|
138
|
+
if (next === state && note === message) return;
|
|
139
|
+
if (next !== state) since = new Date(now()).toISOString();
|
|
140
|
+
state = next; message = note;
|
|
141
|
+
if (!current()) return;
|
|
142
|
+
write(); void report().catch(() => {});
|
|
143
|
+
};
|
|
144
|
+
const follow = (ctx) => {
|
|
145
|
+
cwd = ctx?.cwd ?? cwd;
|
|
146
|
+
try { sessionId = ctx?.sessionManager?.getSessionId?.() ?? sessionId; } catch { /* keep the last one */ }
|
|
147
|
+
};
|
|
148
|
+
// A dialog asks while a turn runs or between turns: the state under it comes back.
|
|
149
|
+
const settle = (next, note) => { settled = next; settledNote = note; if (!prompts) set(next, note); };
|
|
150
|
+
|
|
151
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
152
|
+
currentInstance = instance; started = true; follow(ctx);
|
|
153
|
+
// The Terminal that held this pane before (a crash, `piagent reload`) is replaced.
|
|
154
|
+
if (herdr) for (const row of readJsonFiles(sessionStatusDir(agentDir))) {
|
|
155
|
+
if (row.value.pane === herdr.pane && row.value.pid !== pid && !liveRow(row.value, now())?.alive) fs.rmSync(row.file, { force: true });
|
|
156
|
+
}
|
|
157
|
+
failure = null; prompts = 0; settledNote = undefined;
|
|
158
|
+
settled = ctx?.isIdle?.() === false || managed?.()?.activePrompt ? "processing" : "idle";
|
|
159
|
+
// One company message is many rounds; only the turn's own end counts.
|
|
160
|
+
unsubscribe?.();
|
|
161
|
+
unsubscribe = managed?.()?.session?.subscribe?.(event => {
|
|
162
|
+
if (!current()) return;
|
|
163
|
+
if (event?.type === "managed_provider_retry") settle("processing", `Retrying ${event.attempt}/${event.of}: ${event.code}`);
|
|
164
|
+
if (event?.type === "managed_turn_end") settle(...companyTurnEnd(event.end, event.text));
|
|
165
|
+
});
|
|
166
|
+
state = settled; message = undefined; since = new Date(now()).toISOString();
|
|
167
|
+
write(); await report().catch(() => {});
|
|
168
|
+
clearInterval(timer);
|
|
169
|
+
timer = setInterval(() => { if (!current()) { clearInterval(timer); return; } write(); void report().catch(() => {}); }, heartbeatMs);
|
|
170
|
+
timer.unref?.();
|
|
171
|
+
});
|
|
172
|
+
pi.on("agent_start", (_event, ctx) => { follow(ctx); failure = null; if (settled !== "processing" || !managed?.()?.activePrompt) settle("processing"); });
|
|
173
|
+
pi.on("agent_end", (event) => { failure = failureOf(event?.messages); });
|
|
174
|
+
pi.on("agent_settled", (event, ctx) => {
|
|
175
|
+
if (ctx?.isIdle?.() !== true || managed?.()?.activePrompt) return;
|
|
176
|
+
if (event?.aborted) settle("paused", "Stopped by the member");
|
|
177
|
+
else if (failure) settle("failed", failure);
|
|
178
|
+
else settle("done");
|
|
179
|
+
});
|
|
180
|
+
pi.on("ui_prompt_start", (event) => { prompts += 1; set("paused", clip(String(event?.title || "Waiting for your answer"), 80)); });
|
|
181
|
+
pi.on("ui_prompt_end", () => {
|
|
182
|
+
prompts = Math.max(0, prompts - 1);
|
|
183
|
+
if (!prompts) set(settled, settledNote);
|
|
184
|
+
});
|
|
185
|
+
pi.on("session_shutdown", async () => {
|
|
186
|
+
clearInterval(timer); unsubscribe?.(); unsubscribe = undefined;
|
|
187
|
+
if (currentInstance !== instance) return;
|
|
188
|
+
started = false;
|
|
189
|
+
try { fs.rmSync(file, { force: true }); } catch { /* a later reader prunes it */ }
|
|
190
|
+
await reportSpaces().catch(() => {});
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// `pi --mode rpc|json|print` also loads the package: only a Terminal someone
|
|
196
|
+
// looks at is a session to follow.
|
|
197
|
+
export function onlyInTerminal(pi) {
|
|
198
|
+
let terminal = false;
|
|
199
|
+
return {
|
|
200
|
+
...pi,
|
|
201
|
+
getSessionName: () => pi.getSessionName?.(),
|
|
202
|
+
events: { on: (name, handler) => pi.events?.on?.(name, data => { if (terminal) return handler(data); }) },
|
|
203
|
+
on: (name, handler) => pi.on(name, (event, ctx) => {
|
|
204
|
+
if (name === "session_start") terminal = ctx?.mode === "tui";
|
|
205
|
+
if (terminal) return handler(event, ctx);
|
|
206
|
+
}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// `piagent` itself. The launcher marks it; `piagent studio` registers its own
|
|
211
|
+
// (scripts/piagent-studio.mjs), and a stock `pi` that loads the package stays out.
|
|
212
|
+
export function registerTerminalSessionStatus(pi, env = process.env) {
|
|
213
|
+
if (env.PIAGENT_ACTIVE !== "1") return;
|
|
214
|
+
const terminal = onlyInTerminal(pi), herdr = herdrEnvironment(env);
|
|
215
|
+
sessionStatusExtension({ agentDir: piagentAgentDir(env), herdr })(terminal);
|
|
216
|
+
if (herdr) herdrReporterExtension(herdr, { source: "custom:piagent", resume: (_cwd, file) => (file ? ["piagent", "--session", file] : ["piagent"]) })(terminal);
|
|
217
|
+
}
|