@piagent/platform 1.28.0 → 1.28.2

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 (72) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -1
  3. package/README.vi.md +1 -1
  4. package/docs/command-reference-vietnamese.md +2 -2
  5. package/docs/distribution-standard.md +6 -6
  6. package/docs/languages.json +2 -2
  7. package/docs/operator-manual-vietnamese.md +7 -7
  8. package/docs/publishing-for-teams.md +2 -0
  9. package/docs/release-install-policy.md +2 -2
  10. package/docs/team-onboarding.md +2 -2
  11. package/docs-site/architecture.html +1 -1
  12. package/docs-site/benchmark.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/content/en/index.html +4 -4
  15. package/docs-site/content/en/whats-new.html +12 -0
  16. package/docs-site/content/index.html +4 -4
  17. package/docs-site/content/whats-new.html +12 -0
  18. package/docs-site/context-engine.html +1 -1
  19. package/docs-site/ecosystem.html +1 -1
  20. package/docs-site/en/architecture.html +1 -1
  21. package/docs-site/en/benchmark.html +1 -1
  22. package/docs-site/en/commands.html +1 -1
  23. package/docs-site/en/context-engine.html +1 -1
  24. package/docs-site/en/ecosystem.html +1 -1
  25. package/docs-site/en/index.html +5 -5
  26. package/docs-site/en/mcp-approval.html +1 -1
  27. package/docs-site/en/mcp-auth.html +1 -1
  28. package/docs-site/en/mcp-servers.html +1 -1
  29. package/docs-site/en/mcp.html +1 -1
  30. package/docs-site/en/permissions.html +1 -1
  31. package/docs-site/en/profiles.html +1 -1
  32. package/docs-site/en/quickstart.html +1 -1
  33. package/docs-site/en/runtime.html +1 -1
  34. package/docs-site/en/scope.html +1 -1
  35. package/docs-site/en/security.html +1 -1
  36. package/docs-site/en/team.html +1 -1
  37. package/docs-site/en/whats-new.html +13 -1
  38. package/docs-site/en/windows.html +1 -1
  39. package/docs-site/en/workflows.html +1 -1
  40. package/docs-site/index.html +5 -5
  41. package/docs-site/mcp-approval.html +1 -1
  42. package/docs-site/mcp-auth.html +1 -1
  43. package/docs-site/mcp-servers.html +1 -1
  44. package/docs-site/mcp.html +1 -1
  45. package/docs-site/permissions.html +1 -1
  46. package/docs-site/profiles.html +1 -1
  47. package/docs-site/quickstart.html +1 -1
  48. package/docs-site/runtime.html +1 -1
  49. package/docs-site/scope.html +1 -1
  50. package/docs-site/security.html +1 -1
  51. package/docs-site/team.html +1 -1
  52. package/docs-site/whats-new.html +13 -1
  53. package/docs-site/windows.html +1 -1
  54. package/docs-site/workflows.html +1 -1
  55. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  56. package/package.json +1 -1
  57. package/packages/piagent-core/extensions/company-session-guard.ts +36 -0
  58. package/packages/piagent-core/extensions/piagent-guard.ts +2 -0
  59. package/packages/piagent-core/managed/binding-wait.mjs +66 -0
  60. package/packages/piagent-core/package.json +1 -1
  61. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-BpNzOP6f.js → MarkdownMessage-BKK5r1UP.js} +1 -1
  62. package/packages/piagent-webui/dist/client/assets/{index-DWkb1mpZ.js → index-8YNd9KDA.js} +4 -4
  63. package/packages/piagent-webui/dist/client/index.html +1 -1
  64. package/packages/piagent-webui/gateway/managed-launch.ts +6 -1
  65. package/packages/piagent-webui/shared/company-copy.ts +2 -0
  66. package/scripts/company-terminal.mjs +13 -1
  67. package/scripts/install-global.sh +11 -0
  68. package/scripts/personal-pi-config.mjs +137 -0
  69. package/scripts/piagent-cli.mjs +8 -0
  70. package/scripts/piagent-studio.mjs +25 -6
  71. package/scripts/team-doctor.sh +4 -0
  72. package/scripts/update-global.mjs +9 -2
@@ -7,7 +7,7 @@
7
7
  <meta name="csp-nonce" content="__PIAGENT_CSP_NONCE__" />
8
8
  <meta name="piagent-webui-mode" content="__PIAGENT_WEBUI_MODE__" />
9
9
  <title>Piagent WebUI</title>
10
- <script type="module" crossorigin src="/assets/index-DWkb1mpZ.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-8YNd9KDA.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-B5XOKQy-.css">
12
12
  </head>
13
13
  <body>
@@ -17,6 +17,7 @@ type Binding = { file: string; profile: string; node: string; entrypoint: string
17
17
 
18
18
  const CLEAN_ENV = { PATH: "/usr/bin:/bin", LANG: "en_US.UTF-8", TERM: "xterm-256color", ...interopEnvironment() };
19
19
  const READY = /^Agent Watch Auto: ready$/m;
20
+ const BINDING_WAIT = /\[managed-binding-wait\]$/m, BINDING_WAIT_MS = 95_000;
20
21
  const sha256 = (file: string) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
21
22
  const HEX64 = /^[a-f0-9]{64}$/;
22
23
 
@@ -130,8 +131,12 @@ async function start(agentDir: string, home: string, timeoutMs: number): Promise
130
131
  child.once("exit", () => { exited = true; }); child.once("error", () => { exited = true; }); child.unref();
131
132
  } finally { fs.closeSync(fd); }
132
133
  try {
133
- for (const started = Date.now(); Date.now() - started < timeoutMs;) {
134
+ // Right after an update the entrypoint waits (up to 90 s) for Agent Watch
135
+ // to write the new binding; give it that long before timing out.
136
+ let limit = timeoutMs;
137
+ for (const started = Date.now(); Date.now() - started < limit;) {
134
138
  const text = fs.readFileSync(log, "utf8");
139
+ if (limit === timeoutMs && BINDING_WAIT.test(text)) limit = timeoutMs + BINDING_WAIT_MS;
135
140
  // The launch may just have pointed this slot at the member's store.
136
141
  if (READY.test(text)) return controlSocket(home, pinned.profile);
137
142
  if (exited) throw new Error(text.match(/Agent Watch: (managed-[a-z:_-]+)\./)?.[1] ?? "managed-launch-failed");
@@ -12,6 +12,8 @@ export const LAUNCH_REASONS: Record<string, [string, string]> = {
12
12
  "managed-key-unavailable": ["Agent Watch không đọc được key công ty trên máy này. Mở Agent Watch, dán lại mã kết nối, bấm Kết nối Piagent trong WSL rồi kết nối lại.", "Agent Watch cannot read the company key on this computer. Open Agent Watch, paste the connection code again, choose Connect Piagent in WSL, then connect again."],
13
13
  "managed-profile-disconnected": ["Cấu hình Piagent đang thuộc key công ty đã ngắt kết nối. Nhập lại cấu hình cho Piagent trong Agent Watch (trên Windows: Kết nối Piagent trong WSL).", "The Piagent configuration belongs to a disconnected company key. Import it again in Agent Watch (on Windows: Connect Piagent in WSL)."],
14
14
  "managed-launch-binding-changed": ["Agent Watch hoặc Piagent vừa cập nhật. Nhập lại cấu hình cho Piagent trong Agent Watch (trên Windows: Kết nối Piagent trong WSL).", "Agent Watch or Piagent changed. Import the Piagent configuration again in Agent Watch (on Windows: Connect Piagent in WSL)."],
15
+ "managed-binding-update-pending": ["Agent Watch đang chạy nhưng chưa cập nhật cấu hình cho bản Piagent mới. Mở Agent Watch → Studio, bấm Áp dụng cho Piagent rồi mở lại.", "Agent Watch is running but has not updated the configuration for the new Piagent yet. Open Agent Watch → Studio, apply for Piagent, then open it again."],
16
+ "managed-binding-watch-not-running": ["Piagent vừa cập nhật nhưng Agent Watch không chạy nên chưa cập nhật cấu hình. Mở Agent Watch, chờ vài giây rồi mở lại.", "Piagent was updated but Agent Watch is not running, so the configuration was not updated. Open Agent Watch, wait a few seconds, then open it again."],
15
17
  "managed-launch-version-mismatch": ["Agent Watch đang dùng một bản Piagent khác với dashboard này. Mở Agent Watch → Studio, bấm Áp dụng cho Piagent rồi kết nối lại.", "Agent Watch points at another Piagent install than this dashboard. Open Agent Watch → Studio, apply for Piagent, then connect again."],
16
18
  "managed-launch-stale-runtime": ["Phiên công ty của bản Piagent cũ chưa dừng được. Thử kết nối lại sau ít giây.", "The company runtime from the previous Piagent did not stop yet. Connect again in a few seconds."],
17
19
  "managed-launch-timeout": ["Phiên công ty khởi động quá lâu. Kết nối lại.", "The company runtime took too long to start. Connect again."],
@@ -15,6 +15,12 @@ import { headlineText, outcomeText, processItems } from '../packages/piagent-web
15
15
  import { turnEndCopy, turnEndOf } from '../packages/piagent-webui/shared/turn-end.ts';
16
16
 
17
17
  const LOCALE = 'vi';
18
+ export const HELP_TEXT = [
19
+ 'Cuộc trò chuyện công ty: /undo hoàn tác file lượt trước đã sửa · /bypass on|off|status quyền lệnh cần internet · /fast on|off|status (khi Harness cho phép) · /skill:<tên> hoặc /<lệnh> của bạn và của project.',
20
+ 'Pi: /session thông tin cuộc trò chuyện · /resume mở cuộc trò chuyện công ty khác · /new cuộc mới · /compact nén ngữ cảnh · /tree, /fork nhánh · /name đặt tên · /copy, /export · /hotkeys toàn bộ phím tắt · /quit thoát.',
21
+ 'Phím: ! <lệnh> chạy shell · Esc dừng lượt đang chạy · Ctrl+C xoá ô nhập, hai lần để thoát · Ctrl+O xem thêm.',
22
+ 'Mở lại sau: piagent studio --project <thư mục>, rồi /resume. Đừng mở cuộc trò chuyện công ty bằng pi cá nhân.',
23
+ ].join('\n');
18
24
  const PERMISSION_STATUS = 'agent-watch-permission';
19
25
  const ASK = 'Hỏi trước (mặc định)', BYPASS = 'Bypass: chỉ hỏi việc bắt buộc';
20
26
  const PERMISSION_TEXT = {
@@ -62,7 +68,7 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
62
68
  };
63
69
 
64
70
  registerTerminalUpdateOffer(pi, { installed, packageRoot, restart: 'piagent studio',
65
- afterUpdate: () => bindingChanged() ? 'Bản mới đổi trình khởi chạy công ty: mở Agent Watch → Studio và nhập lại cấu hình cho Piagent trước khi mở lại.' : null });
71
+ afterUpdate: () => bindingChanged() ? 'Bản mới đổi trình khởi chạy công ty: Agent Watch tự cập nhật cấu hình (cần Agent Watch đang chạy); piagent studio chờ việc này khi mở lại.' : null });
66
72
 
67
73
  // Ask first or Bypass, kept with the conversation like the dashboard's switch.
68
74
  const showPermission = (ctx) => ctx.ui?.setStatus?.(PERMISSION_STATUS, managed()?.permission === 'trusted-full-access' ? 'Bypass' : undefined);
@@ -117,6 +123,12 @@ export function companyTerminalExtension({ managed, packageRoot, sdkRoot, bindin
117
123
  : 'Fast đang tắt: các lượt chạy tốc độ thường.', 'info');
118
124
  } });
119
125
 
126
+ // /help is answered here, as on the dashboard: no model request (Pi has no
127
+ // /help of its own, so the text would otherwise go to the agent).
128
+ pi.registerCommand('help', {
129
+ description: 'Lệnh và phím tắt của Terminal công ty (không gửi model)',
130
+ handler: async (_args, ctx) => ctx.ui.notify(HELP_TEXT, 'info') });
131
+
120
132
  // The Harness's notes carry English for the agent; the member reads them in Vietnamese.
121
133
  pi.registerMessageRenderer('agent-watch-process', (message, _options, theme) => {
122
134
  const lines = processLines(message.details);
@@ -781,6 +781,17 @@ if [[ "$WITH_HERDR" == true ]]; then
781
781
  fi
782
782
  fi
783
783
 
784
+ # Personal Pi must not load Piagent twice (an extension path into another
785
+ # Piagent copy beside the package) or keep old Agent Watch company providers:
786
+ # Pi refuses to start, or personal Pi reaches Studio outside the Harness.
787
+ echo
788
+ echo "Personal Pi configuration:"
789
+ if [[ "$DRY_RUN" == true ]]; then
790
+ print_cmd node "$PLATFORM_ROOT/scripts/personal-pi-config.mjs" repair
791
+ else
792
+ node "$PLATFORM_ROOT/scripts/personal-pi-config.mjs" repair || echo "WARN: could not check the personal Pi configuration; run: node \"$PLATFORM_ROOT/scripts/personal-pi-config.mjs\" check" >&2
793
+ fi
794
+
784
795
  echo
785
796
  echo "Installed packages:"
786
797
  if [[ "$DRY_RUN" == true ]]; then
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ // What in the member's personal Pi configuration (~/.pi/agent) conflicts with
3
+ // Piagent, and the repair. Personal Pi loads Piagent as a Pi package; an
4
+ // extension path that points into another Piagent copy registers the same
5
+ // tools twice and Pi refuses to start ("Tool … conflicts with …"). Older Agent
6
+ // Watch releases also wrote company providers (agent_watch_*, a direct key
7
+ // read from Agent Watch) and a company default model into personal Pi; with a
8
+ // managed binding (agent-watch-managed.json) the company runs only through
9
+ // `piagent studio`, so those entries are leftovers that send personal Pi to
10
+ // Studio outside the team's Harness, or fail.
11
+ //
12
+ // personal-pi-config.mjs check [--agent-dir <dir>] [--json] exit 1 when something conflicts
13
+ // personal-pi-config.mjs repair [--agent-dir <dir>] backs up, then removes what conflicts
14
+ import fs from "node:fs";
15
+ import os from "node:os";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const COMPANY_PROVIDER = /^agent_watch_/;
20
+ const SESSION_HEADER_EXTENSION = "agentwatch-studio-session.mjs";
21
+
22
+ const readJson = (file) => { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return null; } };
23
+ const real = (file) => { try { return fs.realpathSync(file); } catch { return null; } };
24
+
25
+ // The Piagent package a path belongs to (its root), or null.
26
+ export function piagentRootOf(file) {
27
+ let dir = real(file);
28
+ if (!dir) return null;
29
+ if (!fs.statSync(dir).isDirectory()) dir = path.dirname(dir);
30
+ for (; dir !== path.dirname(dir); dir = path.dirname(dir)) {
31
+ const manifest = readJson(path.join(dir, "package.json"));
32
+ if (manifest?.name === "@piagent/platform") return dir;
33
+ }
34
+ return null;
35
+ }
36
+
37
+ const isPiagentSource = (source) => /^(git:github\.com\/Vt-mmm\/piagent|https:\/\/github\.com\/Vt-mmm\/piagent|npm:@piagent\/platform)(@|$)/.test(String(source));
38
+
39
+ // The installed Pi package's version (git:… sources live under <agentDir>/git/…).
40
+ export function packageVersion(agentDir, source) {
41
+ const match = /^git:([^@]+)/.exec(String(source));
42
+ if (match) return readJson(path.join(agentDir, "git", match[1], "package.json"))?.version ?? null;
43
+ const npm = /^npm:(@piagent\/platform)/.exec(String(source));
44
+ if (npm) return readJson(path.join(agentDir, "npm", "node_modules", npm[1], "package.json"))?.version ?? null;
45
+ return null;
46
+ }
47
+
48
+ export function inspect(agentDir) {
49
+ const settingsFile = path.join(agentDir, "settings.json"), modelsFile = path.join(agentDir, "models.json");
50
+ const settings = readJson(settingsFile) ?? {}, models = readJson(modelsFile) ?? {};
51
+ const managed = fs.existsSync(path.join(agentDir, "agent-watch-managed.json"));
52
+ const packages = Array.isArray(settings.packages) ? settings.packages : [];
53
+ const piagentPackages = packages.filter(isPiagentSource);
54
+ const extensions = Array.isArray(settings.extensions) ? settings.extensions : [];
55
+ const findings = [];
56
+
57
+ // Extensions inside a Piagent copy load beside the Pi package: duplicate tools.
58
+ const duplicate = extensions.filter((entry) => piagentRootOf(path.resolve(agentDir, String(entry))) && piagentPackages.length);
59
+ if (duplicate.length) findings.push({ kind: "duplicate-extensions", file: settingsFile, entries: duplicate,
60
+ text: `${duplicate.length} extension(s) point into another Piagent copy while the Piagent Pi package is installed: Pi refuses to start (tools registered twice).` });
61
+ if (piagentPackages.length > 1) findings.push({ kind: "duplicate-packages", file: settingsFile, entries: piagentPackages,
62
+ text: `${piagentPackages.length} Piagent Pi packages are registered; keep one.` });
63
+
64
+ if (managed) {
65
+ const providers = Object.keys(models.providers ?? {}).filter((name) => COMPANY_PROVIDER.test(name));
66
+ if (providers.length) findings.push({ kind: "company-providers", file: modelsFile, entries: providers,
67
+ text: `Old Agent Watch company providers (${providers.join(", ")}) are in personal Pi; company work runs only through \`piagent studio\`.` });
68
+ const defaults = [];
69
+ if (COMPANY_PROVIDER.test(String(settings.defaultProvider ?? ""))) defaults.push(`defaultProvider=${settings.defaultProvider}`);
70
+ for (const entry of Array.isArray(settings.enabledModels) ? settings.enabledModels : []) if (COMPANY_PROVIDER.test(String(entry))) defaults.push(`enabledModels:${entry}`);
71
+ if (defaults.length) findings.push({ kind: "company-defaults", file: settingsFile, entries: defaults,
72
+ text: "Personal Pi's default model or model list still names an old company provider." });
73
+ const header = extensions.filter((entry) => path.basename(String(entry)) === SESSION_HEADER_EXTENSION);
74
+ if (header.length) findings.push({ kind: "company-extension", file: settingsFile, entries: header,
75
+ text: "The old Agent Watch session-header extension is still loaded in personal Pi." });
76
+ }
77
+ return { agentDir, managed, findings, packages: piagentPackages.map((source) => ({ source, version: packageVersion(agentDir, source) })) };
78
+ }
79
+
80
+ function backup(file, stamp) {
81
+ if (!fs.existsSync(file)) return null;
82
+ const copy = `${file}.before-piagent-repair-${stamp}`;
83
+ fs.copyFileSync(file, copy);
84
+ fs.chmodSync(copy, 0o600);
85
+ return copy;
86
+ }
87
+
88
+ function writeJson(file, value) {
89
+ const tmp = `${file}.${process.pid}.tmp`;
90
+ fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
91
+ fs.renameSync(tmp, file);
92
+ }
93
+
94
+ // Removes what inspect() found (duplicate packages are left to the installer,
95
+ // which knows the one to keep). Every changed file is backed up first.
96
+ export function repair(agentDir, now = new Date()) {
97
+ const report = inspect(agentDir);
98
+ const kinds = new Set(report.findings.map((finding) => finding.kind));
99
+ const stamp = now.toISOString().replace(/[:.]/g, "-");
100
+ const changed = [], backups = [];
101
+ const settingsFile = path.join(agentDir, "settings.json"), modelsFile = path.join(agentDir, "models.json");
102
+ if (kinds.has("duplicate-extensions") || kinds.has("company-defaults") || kinds.has("company-extension")) {
103
+ const settings = readJson(settingsFile);
104
+ const drop = new Set(report.findings.filter((f) => f.kind === "duplicate-extensions" || f.kind === "company-extension").flatMap((f) => f.entries));
105
+ if (Array.isArray(settings.extensions)) settings.extensions = settings.extensions.filter((entry) => !drop.has(entry));
106
+ if (Array.isArray(settings.extensions) && !settings.extensions.length) delete settings.extensions;
107
+ if (COMPANY_PROVIDER.test(String(settings.defaultProvider ?? ""))) { delete settings.defaultProvider; delete settings.defaultModel; }
108
+ if (Array.isArray(settings.enabledModels)) settings.enabledModels = settings.enabledModels.filter((entry) => !COMPANY_PROVIDER.test(String(entry)));
109
+ backups.push(backup(settingsFile, stamp)); writeJson(settingsFile, settings); changed.push(settingsFile);
110
+ }
111
+ if (kinds.has("company-providers")) {
112
+ const models = readJson(modelsFile);
113
+ for (const name of Object.keys(models.providers ?? {})) if (COMPANY_PROVIDER.test(name)) delete models.providers[name];
114
+ backups.push(backup(modelsFile, stamp)); writeJson(modelsFile, models); changed.push(modelsFile);
115
+ }
116
+ return { ...report, changed, backups: backups.filter(Boolean), after: inspect(agentDir) };
117
+ }
118
+
119
+ const invoked = process.argv[1] && real(process.argv[1]) === real(fileURLToPath(import.meta.url));
120
+ if (invoked) {
121
+ const args = process.argv.slice(2), command = args[0];
122
+ const at = args.indexOf("--agent-dir");
123
+ const agentDir = at >= 0 ? path.resolve(args[at + 1]) : (process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"));
124
+ if (!["check", "repair"].includes(command)) { console.error("Usage: personal-pi-config.mjs check|repair [--agent-dir <dir>] [--json]"); process.exit(2); }
125
+ const result = command === "repair" ? repair(agentDir) : inspect(agentDir);
126
+ if (args.includes("--json")) console.log(JSON.stringify(result, null, 2));
127
+ else if (command === "repair") {
128
+ for (const finding of result.findings) console.log(`fixed: ${finding.text}`);
129
+ for (const file of result.backups) console.log(`backup: ${file}`);
130
+ if (!result.findings.length) console.log("personal Pi configuration: nothing to repair");
131
+ } else {
132
+ for (const finding of result.findings) console.log(`WARN: ${finding.text}`);
133
+ if (result.findings.length) console.log("Fix: piagent-update (or node scripts/personal-pi-config.mjs repair)");
134
+ else console.log("PASS: personal Pi configuration has no Piagent conflicts");
135
+ }
136
+ process.exit(command === "check" && result.findings.length ? 1 : 0);
137
+ }
@@ -158,8 +158,16 @@ child.once("error", (error) => {
158
158
  process.exit(1);
159
159
  });
160
160
 
161
+ // A command stopped from outside (a script, an editor, a timeout) stops its
162
+ // child too, rather than leaving a company run going in the background. A
163
+ // terminal's Ctrl+C already reaches the whole process group: the launcher just
164
+ // stays until the child has finished.
165
+ for (const signal of ["SIGTERM", "SIGHUP"]) process.on(signal, () => { try { child.kill(signal); } catch { /* already gone */ } });
166
+ process.on("SIGINT", () => {});
167
+
161
168
  child.once("exit", (code, signal) => {
162
169
  if (signal) {
170
+ process.removeAllListeners(signal);
163
171
  process.kill(process.pid, signal);
164
172
  return;
165
173
  }
@@ -7,6 +7,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
7
7
  import { ManagedBrokerClient } from '../packages/piagent-core/managed/broker-client.mjs';
8
8
  import { ManagedSession } from '../packages/piagent-core/managed/session.mjs';
9
9
  import { agentWatchDataDirectory, interopEnvironment, resolveStore, storedSlot } from '../packages/piagent-core/managed/store.mjs';
10
+ import { agentWatchRunning, awaitRebind, bindingMismatch, installedAfterBinding } from '../packages/piagent-core/managed/binding-wait.mjs';
10
11
 
11
12
  const digest = file => createHash('sha256').update(fs.readFileSync(file)).digest('hex');
12
13
  // Node's experimental-feature notices (type stripping, SQLite) are not
@@ -30,18 +31,32 @@ Object.assign(process.env, clean);
30
31
  let broker, current, gateway, hold;
31
32
  try {
32
33
  const configPath = fs.realpathSync(flags.get('--config') ?? path.join(home,'.pi/agent/agent-watch-managed.json'));
33
- if (!fs.statSync(configPath).isFile() || fs.statSync(configPath).size > 16384) throw Error('managed-config-invalid');
34
- const config = JSON.parse(fs.readFileSync(configPath));
34
+ const readConfig = () => {
35
+ if (!fs.statSync(configPath).isFile() || fs.statSync(configPath).size > 16384) throw Error('managed-config-invalid');
36
+ return JSON.parse(fs.readFileSync(configPath));
37
+ };
38
+ let config = readConfig();
35
39
  // Agent Watch rewrites this when the key or its harness changes.
36
40
  const bindingRevision = () => { try { return String(JSON.parse(fs.readFileSync(configPath, 'utf8')).configuration_revision ?? '') || null; } catch { return null; } };
37
41
  // --serve backs the personal dashboard: no browser, no project of its own.
38
42
  // Its startup probe runs in an empty private folder that is never listed.
39
43
  const serve = flags.has('--serve');
40
44
  if (serve && (flags.has('--project') || flags.has('--web') || flags.has('--check') || flags.has('--prompt'))) throw Error('managed-launch-arguments');
41
- if (config.schema_version !== 1 || config.model !== 'agent-watch-auto' || !/^[a-f0-9]{64}$/.test(config.profile_id)
42
- || config.entrypoint !== fs.realpathSync(fileURLToPath(import.meta.url)) || config.node !== fs.realpathSync(process.execPath)
43
- || digest(config.entrypoint) !== config.entrypoint_sha256 || digest(config.node) !== config.node_sha256
44
- || digest(config.broker) !== config.broker_sha256) throw Error('managed-launch-binding-changed');
45
+ const running = { self: fs.realpathSync(fileURLToPath(import.meta.url)), node: fs.realpathSync(process.execPath) };
46
+ const mismatch = bindingMismatch(config, running);
47
+ // Right after a Piagent update only the launcher's hash differs until Agent
48
+ // Watch writes the new binding: wait for it. Nothing runs before every
49
+ // pinned path and hash matches again.
50
+ if (mismatch === 'entrypoint' && installedAfterBinding(configPath, config.entrypoint)) {
51
+ const watch = () => agentWatchRunning(config.broker);
52
+ const rebound = await awaitRebind({ running: watch,
53
+ current: () => { try { const next = readConfig(); return bindingMismatch(next, running) === null ? next : null; } catch { return null; } },
54
+ waiting: state => process.stderr.write(state === false
55
+ ? 'Piagent vừa cập nhật nhưng Agent Watch không chạy. Mở Agent Watch; Piagent tự tiếp tục khi cấu hình được cập nhật… [managed-binding-wait]\n'
56
+ : 'Agent Watch đang cập nhật cấu hình cho bản Piagent mới… [managed-binding-wait]\n') });
57
+ if (!rebound) throw Error(watch() === false ? 'managed-binding-watch-not-running' : 'managed-binding-update-pending');
58
+ config = rebound;
59
+ } else if (mismatch) throw Error('managed-launch-binding-changed');
45
60
  // What company sessions need on this machine, checked with the real sandbox
46
61
  // and broker; no conversation, no model request.
47
62
  if (flags.has('--doctor')) {
@@ -162,6 +177,10 @@ try {
162
177
  // (updating Pi breaks the Agent Watch import), no package-update, catalog or
163
178
  // install-telemetry calls. Search tools were provisioned above.
164
179
  process.env.PI_OFFLINE = '1'; process.env.PI_SKIP_VERSION_CHECK = '1';
180
+ // Pi's exit line names `pi --session-dir …`, which would open this company
181
+ // conversation in personal Pi; name the company Terminal instead.
182
+ const write = process.stdout.write.bind(process.stdout), project = /^[\w./~:-]+$/.test(cwd) ? cwd : `'${cwd.replaceAll("'", "'\\''")}'`;
183
+ process.stdout.write = (chunk, ...rest) => write(typeof chunk === 'string' && chunk.includes('To resume this session:') ? `Mở lại cuộc trò chuyện này: piagent studio --project ${project}, rồi /resume\n` : chunk, ...rest);
165
184
  await new api.InteractiveMode(host, { verbose: false }).run();
166
185
  }
167
186
  }
@@ -70,6 +70,10 @@ const warnings = [];
70
70
  const { piagentInstallations } = await import(pathToFileURL(path.join(platformRoot, "scripts/piagent-installations.mjs")).href);
71
71
  const installations = piagentInstallations();
72
72
  if(installations.length>1) warnings.push("Multiple Piagent installations detected. Use the pinned Watch launcher; other PATHs may run an older build: "+installations.map(i=>`${i.version} [${i.fingerprint}] ${i.root}`).join("; "));
73
+ // Personal Pi loading Piagent twice does not start; old company providers send it to Studio outside the Harness.
74
+ const { inspect: inspectPersonalPi } = await import(pathToFileURL(path.join(platformRoot, "scripts/personal-pi-config.mjs")).href);
75
+ for (const finding of inspectPersonalPi(process.env.PI_CODING_AGENT_DIR || path.join(process.env.HOME || "", ".pi", "agent")).findings)
76
+ (finding.kind === "duplicate-extensions" ? errors : warnings).push(`personal Pi: ${finding.text} Fix: piagent-update`);
73
77
  const { verifyCapabilityLock } = await import(pathToFileURL(path.join(platformRoot, "packages", "piagent-core", "capabilities", "capability-core.js")).href);
74
78
 
75
79
  function exists(rel) {
@@ -21,6 +21,7 @@ import os from "node:os";
21
21
  import path from "node:path";
22
22
  import { spawnSync } from "node:child_process";
23
23
  import { fileURLToPath } from "node:url";
24
+ import { inspect as inspectPersonalPi } from "./personal-pi-config.mjs";
24
25
 
25
26
  const HELPER_PACKAGE = "@piagent/platform";
26
27
  const HOST_PACKAGE = "@earendil-works/pi-coding-agent";
@@ -430,13 +431,19 @@ function main() {
430
431
  console.log("Pi Agent Platform update");
431
432
  console.log(` helper: ${currentHelper} -> ${targetHelper}${helperNeedsChange ? "" : " (already current)"}`);
432
433
  console.log(` host: ${currentHost ?? "not installed"} -> ${targetHost}${hostNeedsChange ? "" : " (already current)"}`);
433
- console.log(` package: reinstalled from v${targetHelper} as a resolved commit${options.package ? "" : " (skipped)"}`);
434
+ // The Pi package can lag the helper (installed from an older tag), and
435
+ // personal Pi can hold conflicting Piagent or old company entries.
436
+ const personal = inspectPersonalPi(process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent"));
437
+ const installedPackages = personal.packages.map((item) => item.version ?? "unknown");
438
+ const packageNeedsChange = options.package && (options.force || installedPackages.length !== 1 || installedPackages[0] !== targetHelper);
439
+ console.log(` package: ${installedPackages.join(", ") || "not installed"} -> v${targetHelper}${options.package ? packageNeedsChange ? " (reinstalled as a resolved commit)" : " (already current)" : " (skipped)"}`);
440
+ for (const finding of personal.findings) console.log(` personal Pi: ${finding.text}`);
434
441
  if (options.project) {
435
442
  console.log(` project: ${path.resolve(options.project)} (${options.migrate ? "migrate + doctor" : "doctor only"})`);
436
443
  }
437
444
 
438
445
  if (options.check) {
439
- const pending = [helperNeedsChange && "helper", hostNeedsChange && "host"].filter(Boolean);
446
+ const pending = [helperNeedsChange && "helper", hostNeedsChange && "host", packageNeedsChange && "package", personal.findings.length && "personal Pi configuration"].filter(Boolean);
440
447
  console.log(pending.length ? `\nUpdate available: ${pending.join(", ")}.` : "\nEverything is on the target release.");
441
448
  return;
442
449
  }