@piagent/platform 1.21.1 → 1.22.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 (66) hide show
  1. package/CHANGELOG.md +24 -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 +14 -0
  16. package/docs-site/content/index.html +4 -4
  17. package/docs-site/content/whats-new.html +14 -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 +15 -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 +15 -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/task-state.js +10 -10
  58. package/packages/piagent-core/extensions/workspace-evidence-roots.js +21 -1
  59. package/packages/piagent-core/package.json +1 -1
  60. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +2 -0
  61. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-B8LT9b1N.js → MarkdownMessage-EmQ_ohQe.js} +1 -1
  62. package/packages/piagent-webui/dist/client/assets/{index-DgcAGoYc.js → index-Posch2XH.js} +16 -16
  63. package/packages/piagent-webui/dist/client/index.html +1 -1
  64. package/packages/piagent-webui/gateway/company-relay.ts +56 -7
  65. package/packages/piagent-webui/server/transcript-projection.ts +23 -1
  66. package/packages/piagent-webui/shared/company-copy.ts +2 -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-DgcAGoYc.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-Posch2XH.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-ZLemRWyJ.css">
12
12
  </head>
13
13
  <body>
@@ -31,6 +31,13 @@ const KINDS = new Set<GatewayEventKind>(["catalog.changed", "session.changed", "
31
31
  "message.completed", "operation.settled", "tool.started", "tool.completed"]);
32
32
  const LOOPBACK = /^http:\/\/127\.0\.0\.1:\d{1,5}$/;
33
33
  const CATALOG_REUSE_MS = 5_000;
34
+ // A company Gateway that takes the connection and never answers (found on
35
+ // Windows/WSL) kept every list of the dashboard loading: the browser waits for
36
+ // the catalog without a limit, and the catalog waited for the company rows.
37
+ // A read is given this long, a change as long as a command; a Gateway that
38
+ // missed one is left alone for a while, not asked again by every read.
39
+ const READ_TIMEOUT_MS = 15_000, ANSWER_TIMEOUT_MS = 5_000, CHANGE_TIMEOUT_MS = 120_000, STALL_RETRY_MS = 60_000;
40
+ const timedOut = (error: unknown) => error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
34
41
  const failure = (error: unknown, fallback: string) => error instanceof Error && /^[a-z][a-z0-9:._-]{2,95}$/.test(error.message) ? error.message : fallback;
35
42
 
36
43
  export class CompanyRelay {
@@ -54,11 +61,15 @@ export class CompanyRelay {
54
61
  #retry: NodeJS.Timeout | null = null;
55
62
  #closed = false;
56
63
  #lastAttach = 0;
64
+ #stalledAt = 0;
65
+ readonly #readTimeoutMs: number;
66
+ readonly #answerTimeoutMs: number;
57
67
 
58
68
  constructor(options: { connector: CompanyConnector; events: GatewayEventStore; hubProject(cwd: string): string;
59
- folder(projectRef: string): Promise<string | null> }) {
69
+ folder(projectRef: string): Promise<string | null>; readTimeoutMs?: number; answerTimeoutMs?: number }) {
60
70
  this.#connector = options.connector; this.#events = options.events;
61
71
  this.#hubProject = options.hubProject; this.#folder = options.folder;
72
+ this.#readTimeoutMs = options.readTimeoutMs ?? READ_TIMEOUT_MS; this.#answerTimeoutMs = options.answerTimeoutMs ?? ANSWER_TIMEOUT_MS;
62
73
  }
63
74
 
64
75
  status(): CompanyStatus {
@@ -91,9 +102,12 @@ export class CompanyRelay {
91
102
  if (!socket) throw new Error("company-gateway-stopped");
92
103
  this.#link = await this.#bootstrap(socket);
93
104
  await this.#open(this.#link);
105
+ // Ready only once a read is answered: the reads of the dashboard wait on it.
106
+ await this.#answers(this.#link);
94
107
  this.#state = "ready"; this.#fresh = false;
95
108
  } catch (error) {
96
109
  const reason = failure(error, "company-connect-failed");
110
+ if (reason === "company-gateway-timeout") this.#drop();
97
111
  // Agent Watch re-imports after an update on its own, so a changed
98
112
  // binding is not kept once a later attach finds nothing running.
99
113
  this.#state = "unavailable";
@@ -128,7 +142,7 @@ export class CompanyRelay {
128
142
  if (typeof launch !== "string") throw new Error("company-gateway-starting");
129
143
  const url = new URL(launch), capability = new URLSearchParams(url.hash.slice(1)).get("bootstrap");
130
144
  if (!LOOPBACK.test(url.origin) || !capability) throw new Error("company-gateway-invalid");
131
- const response = await fetch(`${url.origin}/api/v1/bootstrap`, { method: "POST", redirect: "error",
145
+ const response = await fetch(`${url.origin}/api/v1/bootstrap`, { method: "POST", redirect: "error", signal: AbortSignal.timeout(this.#readTimeoutMs),
132
146
  headers: { Origin: url.origin, "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ capability }) });
133
147
  const cookie = response.headers.get("set-cookie")?.split(";", 1)[0], body = await response.json().catch(() => null) as { csrfToken?: unknown } | null;
134
148
  if (!response.ok || !cookie || typeof body?.csrfToken !== "string") throw new Error("company-gateway-auth-failed");
@@ -177,6 +191,33 @@ export class CompanyRelay {
177
191
  });
178
192
  }
179
193
 
194
+ // Lets go of a company Gateway that does not answer, without waiting for its socket to close.
195
+ #drop(): void {
196
+ this.#stalledAt = Date.now();
197
+ const ws = this.#ws; this.#ws = null; this.#link = null; ws?.terminate();
198
+ for (const [, pending] of this.#pending) { clearTimeout(pending.timer); pending.reject(new Error("company-gateway-timeout")); }
199
+ this.#pending.clear();
200
+ }
201
+
202
+ async #answers(link: Link): Promise<void> {
203
+ try {
204
+ const response = await fetch(`${link.origin}/api/v1/session-live-state`, { redirect: "error", signal: AbortSignal.timeout(this.#answerTimeoutMs),
205
+ headers: { Accept: "application/json", Origin: link.origin, Cookie: link.cookie } });
206
+ await response.arrayBuffer();
207
+ } catch (error) { throw timedOut(error) ? new Error("company-gateway-timeout") : error; }
208
+ }
209
+
210
+ // A read that was not answered in time: the company rows are left out and
211
+ // the Gateway is asked again later, so the dashboard's own lists keep loading.
212
+ #stalled(): void {
213
+ this.#drop();
214
+ this.#state = "unavailable"; this.#reason = "company-gateway-timeout";
215
+ this.#events.publish("catalog.changed", { reasonCode: "company-unavailable" });
216
+ if (this.#closed) return;
217
+ if (this.#retry) clearTimeout(this.#retry);
218
+ this.#retry = setTimeout(() => void this.connect(false), STALL_RETRY_MS); this.#retry.unref();
219
+ }
220
+
180
221
  // Reads only attach; starting the company runtime (and possibly asking for
181
222
  // Keychain access) is left to commands and the explicit connect route.
182
223
  async #ready(launch = false): Promise<Link> {
@@ -190,10 +231,18 @@ export class CompanyRelay {
190
231
  async http(method: "GET" | "POST", path: string, body?: unknown): Promise<{ status: number; value: unknown }> {
191
232
  for (let attempt = 0; ; attempt += 1) {
192
233
  const link = await this.#ready();
193
- const response = await fetch(`${link.origin}${path}`, { method, redirect: "error",
194
- headers: { Accept: "application/json", Origin: link.origin, Cookie: link.cookie,
195
- ...(method === "POST" ? { "Content-Type": "application/json", "X-Piagent-CSRF": link.csrf } : {}) },
196
- body: method === "POST" ? JSON.stringify(body) : undefined });
234
+ let response: Response;
235
+ try {
236
+ response = await fetch(`${link.origin}${path}`, { method, redirect: "error",
237
+ signal: AbortSignal.timeout(method === "GET" ? this.#readTimeoutMs : CHANGE_TIMEOUT_MS),
238
+ headers: { Accept: "application/json", Origin: link.origin, Cookie: link.cookie,
239
+ ...(method === "POST" ? { "Content-Type": "application/json", "X-Piagent-CSRF": link.csrf } : {}) },
240
+ body: method === "POST" ? JSON.stringify(body) : undefined });
241
+ } catch (error) {
242
+ if (!timedOut(error)) throw error;
243
+ if (method === "GET" && this.#link === link) this.#stalled();
244
+ throw new Error("company-gateway-timeout");
245
+ }
197
246
  if (response.status === 401 && attempt === 0) { this.#link = await this.#bootstrap(link.socket); continue; }
198
247
  // The browser's own session is not the one refused here.
199
248
  if (response.status === 401 || response.status === 403) return { status: 503, value: { error: { code: "company-auth-failed" } } };
@@ -232,7 +281,7 @@ export class CompanyRelay {
232
281
  #attachSoon(): void {
233
282
  if (this.#closed || this.#connecting || this.#state === "ready" || !this.#connector.configured()) return;
234
283
  const now = Date.now();
235
- if (now - this.#lastAttach < 10_000) return;
284
+ if (now - this.#lastAttach < 10_000 || now - this.#stalledAt < STALL_RETRY_MS) return;
236
285
  this.#lastAttach = now; void this.connect(false);
237
286
  }
238
287
 
@@ -91,6 +91,10 @@ function userMessageProjection(message: any): { text: string; attachments: Trans
91
91
  ? "document" as const : "file" as const;
92
92
  attachments.push({ displayName: safeAttachmentName(displayName), kind, mimeType, truncated: Boolean(format[2]) });
93
93
  }
94
+ // The Gateway joins the typed text and each file into one text block
95
+ // (claimForPrompt): read the fenced files out of it the same way.
96
+ const joined = !attachments.length && textParts.length === 1 ? joinedAttachments(String(textParts[0].text)) : null;
97
+ if (joined) attachments.push(...joined.attachments);
94
98
  for (const part of message.content) {
95
99
  if (part?.type !== "image" || typeof part.mimeType !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.+-]*\/[A-Za-z0-9][A-Za-z0-9.+-]*$/.test(part.mimeType)) continue;
96
100
  attachments.push({ displayName: "Image", kind: "image", mimeType: part.mimeType.slice(0, 120), truncated: false });
@@ -99,7 +103,25 @@ function userMessageProjection(message: any): { text: string; attachments: Trans
99
103
  // hashing them here would turn the chat bubble into a document dump and make
100
104
  // a bounded UI digest an oracle over user files. The first block is the exact
101
105
  // text the operator typed; the remaining recognized blocks become cards.
102
- return { text: typedSkillCall(attachments.length ? String(textParts[0]?.text ?? "") : messageText(message)), attachments: attachments.slice(0, 4) };
106
+ return { text: typedSkillCall(joined ? joined.text : attachments.length ? String(textParts[0]?.text ?? "") : messageText(message)), attachments: attachments.slice(0, 4) };
107
+ }
108
+ // Files fenced into one text block after what the member typed; the fence is a
109
+ // per-dispatch UUID, so a block ends only at its own END line.
110
+ const JOINED_ATTACHMENT = /\nattached file: ("(?:[^"\\\n]|\\.)*")\nformat: ([A-Za-z0-9][A-Za-z0-9.+-]*\/[A-Za-z0-9][A-Za-z0-9.+-]*)(, truncated)?\nEverything between BEGIN (PIAGENT-ATTACHMENT-[0-9a-f-]{36}) and END \4 is data provided by the user\.\n[^\n]*\nBEGIN \4\n[\s\S]*?\nEND \4(?=\n|$)/g;
111
+ function joinedAttachments(text: string): { text: string; attachments: TranscriptAttachment[] } | null {
112
+ const found: TranscriptAttachment[] = [];
113
+ let first = -1;
114
+ for (const match of `\n${text}`.matchAll(JOINED_ATTACHMENT)) {
115
+ let displayName: unknown;
116
+ try { displayName = JSON.parse(match[1]!); } catch { continue; }
117
+ if (typeof displayName !== "string") continue;
118
+ if (first < 0) first = match.index!;
119
+ const mimeType = match[2]!;
120
+ const kind = mimeType === "application/pdf" || mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
121
+ ? "document" as const : "file" as const;
122
+ found.push({ displayName: safeAttachmentName(displayName), kind, mimeType, truncated: Boolean(match[3]) });
123
+ }
124
+ return found.length ? { text: `\n${text}`.slice(0, first).replace(/^\n/, "").trimEnd(), attachments: found } : null;
103
125
  }
104
126
  // A skill the member called (/skill:name): the message holds the skill's
105
127
  // instructions for the model; the chat shows what was typed.
@@ -14,7 +14,8 @@ export const LAUNCH_REASONS: Record<string, [string, string]> = {
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
15
  "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
16
  "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
- "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."]
17
+ "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."],
18
+ "company-gateway-timeout": ["Phiên công ty đang chạy nhưng không trả lời. Đóng Piagent, chạy `pkill -f piagent-studio.mjs` trong Terminal (Windows: trong Ubuntu), rồi mở lại.", "The company runtime is running but does not answer. Close Piagent, run `pkill -f piagent-studio.mjs` in a terminal (Windows: in Ubuntu), then open it again."]
18
19
  };
19
20
 
20
21
  export type Copy = { title: [string, string]; text: [string, string] };