@piagent/platform 1.14.0 → 1.15.1

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 (79) hide show
  1. package/CHANGELOG.md +26 -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 +24 -0
  16. package/docs-site/content/index.html +4 -4
  17. package/docs-site/content/whats-new.html +24 -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 +25 -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 +25 -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/managed/broker-client.mjs +7 -1
  58. package/packages/piagent-core/managed/call-purpose.mjs +39 -0
  59. package/packages/piagent-core/managed/helper-roles.mjs +7 -3
  60. package/packages/piagent-core/managed/helper-run.mjs +8 -5
  61. package/packages/piagent-core/managed/patch-snapshot.mjs +29 -7
  62. package/packages/piagent-core/managed/request-stream.mjs +4 -1
  63. package/packages/piagent-core/managed/review-snapshot-worker.mjs +27 -3
  64. package/packages/piagent-core/managed/sandbox-diagnostic.mjs +7 -1
  65. package/packages/piagent-core/managed/session.mjs +42 -14
  66. package/packages/piagent-core/managed/tool-boundary.mjs +5 -1
  67. package/packages/piagent-core/managed/web-search.mjs +1 -1
  68. package/packages/piagent-core/managed/workflow.mjs +19 -5
  69. package/packages/piagent-core/package.json +1 -1
  70. package/packages/piagent-core/runtime/managed-failure.mjs +9 -1
  71. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-CyT2G1H2.js → MarkdownMessage-DjA3OGyI.js} +1 -1
  72. package/packages/piagent-webui/dist/client/assets/{index-BBEa7Ger.js → index-bE2DKmc1.js} +12 -12
  73. package/packages/piagent-webui/dist/client/index.html +1 -1
  74. package/packages/piagent-webui/gateway/pi-host.ts +16 -2
  75. package/packages/piagent-webui/gateway/session-command-controller.ts +11 -0
  76. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +2 -2
  77. package/packages/piagent-webui/ownership/session-lease-store.ts +20 -1
  78. package/scripts/piagent-cli.mjs +5 -1
  79. package/scripts/update-global.mjs +7 -1
@@ -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-BBEa7Ger.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-bE2DKmc1.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-Z91z6SlN.css">
12
12
  </head>
13
13
  <body>
@@ -28,9 +28,23 @@ export function installedPiHostRoot(): string {
28
28
  } catch {
29
29
  // The operator installation is normally global.
30
30
  }
31
+ const windows = process.platform === "win32";
32
+ if (windows) {
33
+ // npm's global prefix is configurable (CI commonly uses C:\npm\prefix).
34
+ // Read the package beside each PATH entry directly: where.exe may be
35
+ // unavailable in a restricted launcher, and an earlier stale shim must
36
+ // not hide a valid installation later on PATH. Windows env keys ignore case.
37
+ const environment = Object.fromEntries(Object.entries(process.env).map(([key, value]) => [key.toLowerCase(), value]));
38
+ const prefixes = [...(environment.path ?? "").split(";"), environment.npm_config_prefix];
39
+ for (const value of prefixes) {
40
+ const prefix = value?.trim().replace(/^"(.*)"$/, "$1");
41
+ if (!prefix || !path.isAbsolute(prefix)) continue;
42
+ const found = packageRootFrom(path.join(prefix, "node_modules", "@earendil-works", "pi-coding-agent", "package.json"));
43
+ if (found) return found;
44
+ }
45
+ }
31
46
  try {
32
- const windows = process.platform === "win32";
33
- const executable = execFileSync(windows ? "where" : "which", ["pi"], { encoding: "utf8", timeout: 2_000, stdio: ["ignore", "pipe", "ignore"] }).trim().split(/\r?\n/)[0];
47
+ const executable = execFileSync(windows ? "where.exe" : "which", ["pi"], { encoding: "utf8", timeout: 2_000, stdio: ["ignore", "pipe", "ignore"] }).trim().split(/\r?\n/)[0];
34
48
  // npm on Windows puts the pi.cmd shim in the global prefix and the package
35
49
  // in node_modules beside it.
36
50
  const found = packageRootFrom(fs.realpathSync(executable))
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import { parseFailure } from "../../piagent-core/runtime/managed-failure.mjs";
2
3
 
3
4
  import type { Catalog, SessionRow } from "../contracts/generated/session-catalog-v1.ts";
4
5
  import type { Receipt } from "../contracts/generated/session-command-v1.ts";
@@ -279,6 +280,16 @@ export class SessionCommandController {
279
280
  try { if (noDispatchEffect) attachmentReservation.release(); else attachmentReservation.commit(); }
280
281
  catch { /* an attempted dispatch with an unprovable reservation remains uncertain */ }
281
282
  }
283
+ // The company runtime of a new or reopened conversation did not open
284
+ // (key, harness, Agent Watch helper): nothing was sent. Say why, by
285
+ // its kind, rather than "resyncing" or a bare "could not restart".
286
+ const company = ["session.create", "session.acquire"].includes(command.action) && /^managed-[a-z]/.test(message) ? parseFailure(message) : null;
287
+ if (company && company.code !== "managed-request-failed") {
288
+ const receipt = this.#rejected(command, after, afterRow, "unavailable", `company-${company.kind}`);
289
+ try { this.#store.settle(command, receipt, this.#now()); } catch { /* intent remains uncertain */ }
290
+ if (command.action === "session.create" && afterRow) this.#events.publish("session.changed", { catalogRevision: after.catalogRevision, session: afterRow });
291
+ return receipt;
292
+ }
282
293
  const code = /recovery/.test(message) ? "recovery-required"
283
294
  : /(owner-conflict|operation-conflict|runtime-busy)/.test(message) ? "owner-conflict"
284
295
  : noDispatchEffect || command.action === "session.send" && !sendAttempted ? "unavailable" : "effect-unknown";
@@ -133,7 +133,7 @@ export class SessionRuntimeSupervisor {
133
133
  const runtimeInstanceRef = `runtime_${randomBytes(24).toString("base64url")}`;
134
134
  const prior = this.#leases.inspect(sessionRef);
135
135
  if (prior.state === "gateway-owned" || prior.state === "terminal-owned" || prior.state === "recovery-required") {
136
- try { this.#leases.releaseDeadOwnerForExplicitRecovery(sessionRef); } catch { /* live or unprovable owners stay authoritative */ }
136
+ try { this.#leases.releaseDeadOwnerForExplicitRecovery(sessionRef, new Date(), this.#gatewayInstanceRef); } catch { /* live or unprovable owners stay authoritative */ }
137
137
  }
138
138
  const lease = this.#leases.acquire(sessionRef, this.#gatewayInstanceRef, runtimeInstanceRef);
139
139
  try {
@@ -161,7 +161,7 @@ export class SessionRuntimeSupervisor {
161
161
  const active = this.#active.get(sessionRef);
162
162
  if (!active) {
163
163
  const current = this.#leases.inspect(sessionRef);
164
- if (current.state === "released") return current;
164
+ if (current.state === "released" || current.state === "recovery-required" && current.gatewayInstanceRef === this.#gatewayInstanceRef) return this.#leases.releaseOwnRecovery(sessionRef, this.#gatewayInstanceRef);
165
165
  throw new Error(current.state === "recovery-required" ? "session-recovery-required" : "session-owner-conflict");
166
166
  }
167
167
  if (active.operationRef) throw new Error("session-runtime-busy");
@@ -253,8 +253,10 @@ export class SessionLeaseStore {
253
253
  return this.#acquireOwner(sessionRef, gatewayInstanceRef, runtimeInstanceRef, now);
254
254
  }
255
255
 
256
- releaseDeadOwnerForExplicitRecovery(sessionRef: string, now = new Date()): SessionLeaseSnapshot {
256
+ // `ownGateway`: the calling Gateway, whose own recovery it may release.
257
+ releaseDeadOwnerForExplicitRecovery(sessionRef: string, now = new Date(), ownGateway?: string): SessionLeaseSnapshot {
257
258
  if (!Number.isFinite(now.getTime())) throw new Error("session-lease-input-invalid");
259
+ if (ownGateway && this.inspect(sessionRef).state === "recovery-required" && this.inspect(sessionRef).gatewayInstanceRef === ownGateway) return this.releaseOwnRecovery(sessionRef, ownGateway, now);
258
260
  return this.#withMutationLock(sessionRef, () => {
259
261
  const current = this.inspect(sessionRef);
260
262
  const terminalOwner = current.state === "terminal-owned"
@@ -329,6 +331,23 @@ export class SessionLeaseStore {
329
331
  });
330
332
  }
331
333
 
334
+ // A Gateway whose own runtime failed to open or to close left the lease in
335
+ // recovery under its own name. Nothing of it runs (the Gateway knows its
336
+ // live runtimes), so that Gateway may release it: archiving or reopening
337
+ // must not wait for the Gateway itself to exit. Another owner's recovery
338
+ // still needs proof that the owner is gone.
339
+ releaseOwnRecovery(sessionRef: string, gatewayInstanceRef: string, now = new Date()): SessionLeaseSnapshot {
340
+ if (!validRef(gatewayInstanceRef) || gatewayInstanceRef.startsWith(TERMINAL_OWNER_PREFIX) || !Number.isFinite(now.getTime())) throw new Error("session-lease-input-invalid");
341
+ return this.#withMutationLock(sessionRef, () => {
342
+ const current = this.inspect(sessionRef);
343
+ if (current.state === "released") return current;
344
+ if (current.state !== "recovery-required" || current.gatewayInstanceRef !== gatewayInstanceRef) throw new Error("session-recovery-required");
345
+ this.#append(sessionRef, { recordedAt: now.toISOString(), sessionRef, event: "released", ownerEpoch: current.ownerEpoch!,
346
+ gatewayInstanceRef, runtimeInstanceRef: current.runtimeInstanceRef!, reasonCode: null });
347
+ return this.inspect(sessionRef);
348
+ });
349
+ }
350
+
332
351
  requireRecovery(sessionRef: string, ownerEpoch: string, gatewayInstanceRef: string, runtimeInstanceRef: string,
333
352
  reasonCode: string, now = new Date()): SessionLeaseSnapshot {
334
353
  if (!validRef(reasonCode)) throw new Error("session-lease-reason-invalid");
@@ -51,6 +51,10 @@ if (invokedAs === "piagent") {
51
51
  } else if (subcommand === "select-verification") {
52
52
  script = "scripts/select-independent-verification.mjs";
53
53
  forwardedArgs = forwardedArgs.slice(1);
54
+ } else if (subcommand && Object.hasOwn(scriptByCommand, `piagent-${subcommand}`)) {
55
+ // `piagent doctor` is `piagent-doctor`: members type it both ways.
56
+ script = scriptByCommand[`piagent-${subcommand}`];
57
+ forwardedArgs = forwardedArgs.slice(1);
54
58
  } else if ([undefined, "help", "--help", "-h"].includes(subcommand)) {
55
59
  // Subcommands are listed, their flags are not. Restating them here meant a
56
60
  // second copy that drifts: this line still advertised the dashboard without
@@ -70,7 +74,7 @@ if (invokedAs === "piagent") {
70
74
  }
71
75
 
72
76
  if (!script) {
73
- console.error(`Unknown Pi Agent command: ${invokedAs || "(unknown)"}`);
77
+ console.error(`Unknown Pi Agent command: ${[invokedAs || "(unknown)", invokedAs === "piagent" ? forwardedArgs[0] : ""].filter(Boolean).join(" ")}`);
74
78
  console.error(`Expected one of: ${Object.keys(scriptByCommand).sort().join(", ")}`);
75
79
  process.exit(2);
76
80
  }
@@ -423,7 +423,8 @@ function main() {
423
423
  fail(`${HELPER_PACKAGE}@${targetHelper} does not pin an exact ${HOST_PACKAGE} version, so the host cannot be updated safely.`);
424
424
  }
425
425
 
426
- const helperNeedsChange = options.force || currentHelper !== targetHelper;
426
+ // npm exec is temporary even when its version already matches the target.
427
+ const helperNeedsChange = options.force || currentHelper !== targetHelper || isNpxCacheRoot(platformRoot);
427
428
  const hostNeedsChange = options.host && (options.force || currentHost !== targetHost);
428
429
 
429
430
  console.log("Pi Agent Platform update");
@@ -475,6 +476,11 @@ function main() {
475
476
  }
476
477
  }
477
478
 
479
+ // Refuse an impossible package installation before replacing either global.
480
+ if (options.package && !options.host && effectiveHost !== targetHost) {
481
+ fail(`--no-host cannot install this Pi package: active Pi is ${effectiveHost ?? "missing"}, but v${targetHelper} requires ${targetHost}. Rerun without --no-host.`);
482
+ }
483
+
478
484
  // Host first: piagent-install refuses to run against a host that does not match
479
485
  // the version its package.json pins, so a helper installed ahead of the host
480
486
  // would fail on its very next step.