@piagent/platform 1.49.0 → 1.50.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/docs/command-reference-vietnamese.md +2 -2
  3. package/docs/distribution-standard.md +6 -6
  4. package/docs/harness-conformance.md +3 -3
  5. package/docs/operator-manual-vietnamese.md +7 -7
  6. package/docs/publishing-for-teams.md +2 -0
  7. package/docs/release-install-policy.md +2 -2
  8. package/docs/team-onboarding.md +2 -2
  9. package/docs-site/architecture.html +1 -1
  10. package/docs-site/assets/search-en.json +1 -1
  11. package/docs-site/assets/search-vi.json +1 -1
  12. package/docs-site/benchmark.html +1 -1
  13. package/docs-site/commands.html +1 -1
  14. package/docs-site/context-engine.html +1 -1
  15. package/docs-site/ecosystem.html +1 -1
  16. package/docs-site/en/architecture.html +1 -1
  17. package/docs-site/en/benchmark.html +1 -1
  18. package/docs-site/en/commands.html +1 -1
  19. package/docs-site/en/context-engine.html +1 -1
  20. package/docs-site/en/ecosystem.html +1 -1
  21. package/docs-site/en/index.html +15 -15
  22. package/docs-site/en/mcp-approval.html +1 -1
  23. package/docs-site/en/mcp-auth.html +1 -1
  24. package/docs-site/en/mcp-servers.html +1 -1
  25. package/docs-site/en/mcp.html +1 -1
  26. package/docs-site/en/permissions.html +1 -1
  27. package/docs-site/en/profiles.html +1 -1
  28. package/docs-site/en/quickstart.html +1 -1
  29. package/docs-site/en/runtime.html +1 -1
  30. package/docs-site/en/scope.html +1 -1
  31. package/docs-site/en/security.html +1 -1
  32. package/docs-site/en/team.html +1 -1
  33. package/docs-site/en/update.html +3 -3
  34. package/docs-site/en/whats-new.html +56 -27
  35. package/docs-site/en/windows.html +1 -1
  36. package/docs-site/en/workflows.html +1 -1
  37. package/docs-site/index.html +15 -15
  38. package/docs-site/mcp-approval.html +1 -1
  39. package/docs-site/mcp-auth.html +1 -1
  40. package/docs-site/mcp-servers.html +1 -1
  41. package/docs-site/mcp.html +1 -1
  42. package/docs-site/permissions.html +1 -1
  43. package/docs-site/profiles.html +1 -1
  44. package/docs-site/quickstart.html +1 -1
  45. package/docs-site/releases.json +97 -0
  46. package/docs-site/runtime.html +1 -1
  47. package/docs-site/scope.html +1 -1
  48. package/docs-site/security.html +1 -1
  49. package/docs-site/team.html +1 -1
  50. package/docs-site/update.html +3 -3
  51. package/docs-site/whats-new.html +56 -27
  52. package/docs-site/windows.html +1 -1
  53. package/docs-site/workflows.html +1 -1
  54. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  55. package/package.json +1 -1
  56. package/packages/piagent-core/managed/approval-refusal.mjs +9 -0
  57. package/packages/piagent-core/managed/check-evidence.mjs +134 -0
  58. package/packages/piagent-core/managed/check-questions.mjs +32 -14
  59. package/packages/piagent-core/managed/command-tools.mjs +7 -3
  60. package/packages/piagent-core/managed/fast-mode.mjs +19 -16
  61. package/packages/piagent-core/managed/helper-roles.mjs +1 -1
  62. package/packages/piagent-core/managed/helper-run.mjs +21 -3
  63. package/packages/piagent-core/managed/ledger.mjs +5 -5
  64. package/packages/piagent-core/managed/memory-lessons.mjs +2 -2
  65. package/packages/piagent-core/managed/repository-operation.mjs +2 -1
  66. package/packages/piagent-core/managed/request-stream.mjs +2 -2
  67. package/packages/piagent-core/managed/session.mjs +2 -2
  68. package/packages/piagent-core/managed/tool-boundary.mjs +1 -1
  69. package/packages/piagent-core/managed/tool-failures.mjs +1 -1
  70. package/packages/piagent-core/managed/workflow.mjs +14 -8
  71. package/packages/piagent-core/package.json +1 -1
  72. package/packages/piagent-core/runtime/inspection/approval-broker.ts +2 -2
  73. package/packages/piagent-webui/dist/client/assets/ActivityPanel-CmjxPmNT.js +1 -0
  74. package/packages/piagent-webui/dist/client/assets/{App-CU-wndsb.js → App-JrRb7jgn.js} +1 -1
  75. package/packages/piagent-webui/dist/client/assets/GatewayApp-BmkqeXih.js +63 -0
  76. package/packages/piagent-webui/dist/client/assets/{InspectionApp-Bfvr2S8S.js → InspectionApp-9jBsgkVJ.js} +1 -1
  77. package/packages/piagent-webui/dist/client/assets/{SessionAgentWorkspace-0Xowe-sG.js → SessionAgentWorkspace-RQ0N65d9.js} +1 -1
  78. package/packages/piagent-webui/dist/client/assets/{SessionInspectorDrawer-72Z5908P.js → SessionInspectorDrawer-BWyEN4bf.js} +2 -2
  79. package/packages/piagent-webui/dist/client/assets/{SettingsNavigation-Bu1A7x4F.js → SettingsNavigation-Wx-q5QQk.js} +1 -1
  80. package/packages/piagent-webui/dist/client/assets/{SettingsPage-DxGxE0AF.js → SettingsPage-xAt6kSyZ.js} +1 -1
  81. package/packages/piagent-webui/dist/client/assets/{TerminalPanel-BKgx9SjR.js → TerminalPanel-DzHdSjpN.js} +1 -1
  82. package/packages/piagent-webui/dist/client/assets/{WorkspaceCodeEditor-DFrh3qs2.js → WorkspaceCodeEditor-CEuPTTBd.js} +1 -1
  83. package/packages/piagent-webui/dist/client/assets/{WorkspaceExplorer-D6ufO-eP.js → WorkspaceExplorer-B_NwYf4S.js} +3 -3
  84. package/packages/piagent-webui/dist/client/assets/{index-CjM70M7D.css → index-De7bIeMc.css} +1 -1
  85. package/packages/piagent-webui/dist/client/assets/{index-C3CJv1wM.js → index-fUpioGMZ.js} +2 -2
  86. package/packages/piagent-webui/dist/client/index.html +2 -2
  87. package/packages/piagent-webui/gateway/gateway-session-stream.ts +28 -0
  88. package/packages/piagent-webui/gateway/managed-projection.ts +8 -1
  89. package/packages/piagent-webui/gateway/session-catalog.ts +2 -2
  90. package/packages/piagent-webui/server/tool-call-summary.ts +13 -0
  91. package/packages/piagent-webui/server/transcript-projection.ts +11 -4
  92. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +2 -0
  93. package/schemas/piagent-webui/session-catalog-v1.schema.json +1 -1
  94. package/schemas/piagent-webui/transcript-v1.schema.json +17 -1
  95. package/scripts/company-terminal.mjs +2 -2
  96. package/packages/piagent-webui/dist/client/assets/ActivityPanel-Bv-jc1s3.js +0 -1
  97. package/packages/piagent-webui/dist/client/assets/GatewayApp-MIvgr7kn.js +0 -63
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { beforeCheckOrPush } from './check-questions.mjs';
3
+ import { approvalRefusal } from './approval-refusal.mjs';
3
4
  import { asksOnlyHandoff, broadAccess, effectivePermission, networkConfirmation } from './permission.mjs';
4
5
  import { handoffReason } from '../runtime/policy/handoff-policy.mjs';
5
6
  import { engineAlive, engineLauncher } from './container-engine.mjs';
@@ -38,7 +39,7 @@ const KINDS = {
38
39
  },
39
40
  host: {
40
41
  name: 'run_on_host', label: 'Run on this machine', scope: 'host-command-once', provider: 'host', riskClass: 'high',
41
- description: 'Run one shell command on the member\'s machine, outside the sandbox, as the member, after they approve that exact command (only Bypass runs it without asking, and even there a push, PR, publish, deploy or a command that reaches credentials is asked). Use it when the sandbox blocks what the task needs and no other tool covers it: install a runtime or tool the project needs (brew install openjdk@21, brew install node@22, sdk install java…), git pull with the member\'s sign-in, git push only when the member asked for it, a private registry, a file outside the project. It has the member\'s files, Keychain and sign-ins, so ask only for what the task needs and say why in reason. No input can be typed: commands that need the member\'s password (sudo, a .pkg installer) or an interactive sign-in are refused; ask the member with ask_user for those.',
42
+ description: 'Run one shell command on the member\'s machine, outside the sandbox, as the member, in their login shell (zsh on macOS: an unquoted $var is not split into words, unlike bash), after they approve that exact command (only Bypass runs it without asking, and even there a push, PR, publish, deploy or a command that reaches credentials is asked). Use it when the sandbox blocks what the task needs and no other tool covers it: install a runtime or tool the project needs (brew install openjdk@21, brew install node@22, sdk install java…), git pull with the member\'s sign-in, git push only when the member asked for it, a private registry, a file outside the project. It has the member\'s files, Keychain and sign-ins, so ask only for what the task needs and say why in reason. No input can be typed: commands that need the member\'s password (sudo, a .pkg installer) or an interactive sign-in are refused; ask the member with ask_user for those.',
42
43
  allow: 'Run this exact command once on this machine, outside the sandbox, as you: it can read and change your files and use your sign-ins.',
43
44
  title: 'Chạy lệnh trên máy (ngoài sandbox)', allowText: 'chạy đúng lệnh này một lần trên máy bạn, ngoài sandbox, bằng quyền của bạn: lệnh đọc/sửa được file và dùng được tài khoản đã đăng nhập trên máy.',
44
45
  // Full access does not cover it: the member approves each command (with
@@ -78,6 +79,7 @@ export function stillAsks(self, reason) {
78
79
  // member may allow it once, allow the same exact action for the rest of the
79
80
  // conversation (never a handoff or a waiver of checks: its scope names it), deny it, or deny it and
80
81
  // stop the turn so the agent does not try again.
82
+ const HANDOFF_APPROVAL_MS = 30 * 60_000;
81
83
  const CHOICES = { once: 'Đồng ý một lần', session: 'Đồng ý cho cả cuộc trò chuyện (đúng lệnh này)', deny: 'Từ chối', stop: 'Từ chối và dừng lượt này' };
82
84
  export async function approve(self, id, signal, ctx, action, { title, body }) {
83
85
  const grantable = !/handoff|waiver/.test(String(action.requestedScope ?? '')), key = `${action.toolName}\u0000${action.commandPreview ?? JSON.stringify(action.rawAction ?? {})}`;
@@ -91,9 +93,11 @@ export async function approve(self, id, signal, ctx, action, { title, body }) {
91
93
  const picked = await ctx.ui.select(`${title}\n\n${body}`, choices.map(choice => CHOICES[choice]));
92
94
  return choices.find(choice => CHOICES[choice] === picked) ?? 'deny';
93
95
  } } : {}),
96
+ // A push waits longer than other cards: the member may be away while a suite ran.
97
+ ...(/handoff/.test(String(action.requestedScope ?? '')) ? { ttlMs: HANDOFF_APPROVAL_MS } : {}),
94
98
  unavailableFallback: 'terminal-confirm', recheck: () => !signal?.aborted && Boolean(self.grant) });
95
99
  if (!decision.allowed && decision.choice === 'stop') { try { ctx?.abort?.(); } catch { /* the denial below still stands */ } throw Error('managed-operation-denied-stop: bạn đã từ chối và dừng lượt này; không thử lại, chờ member.'); }
96
- if (!decision.allowed || !decision.consume() || signal?.aborted) throw Error('managed-operation-denied');
100
+ if (!decision.allowed || !decision.consume() || signal?.aborted) throw approvalRefusal(decision);
97
101
  if (decision.choice === 'session' && grantable) (self.sessionApprovals ??= new Set()).add(key);
98
102
  }
99
103
 
@@ -126,7 +130,7 @@ function commandTool(self, kind) {
126
130
  } finally {
127
131
  // What the command installed (a JDK, Node) is there for the next one.
128
132
  if (kind === 'host') self.boundary.refreshEnvironment();
129
- await self.claimChanges(before); await self.run?.afterTool('bash', args, ok, () => self.digest()); await self.refreshReview();
133
+ await self.claimChanges(before); await self.run?.afterTool('bash', args, ok, () => self.digest(), self); await self.refreshReview();
130
134
  }
131
135
  } };
132
136
  }
@@ -1,10 +1,12 @@
1
1
  // Fast mode of a company conversation. The member turns it on per
2
2
  // conversation, and only where the team's Harness in Studio allows it
3
3
  // (harness.configuration.fast_mode); a conversation starts with it off. While
4
- // on, the main agent's calls ask a Codex model for the priority service tier:
5
- // the same model and thinking level, answered faster, and charged to the
6
- // key's quota at a higher rate (Studio decides the rate and refuses the tier
7
- // for a run whose Harness does not allow it). Subagents run as usual.
4
+ // on, every call of the conversation, the main agent's and its subagents', asks
5
+ // for the priority service tier where its model serves it: the same model and
6
+ // thinking level, answered faster, and charged to the key's quota at a higher
7
+ // rate (Studio decides the rate and refuses the tier for a run whose Harness
8
+ // does not allow it). A model that does not serve it runs at standard speed,
9
+ // so a Claude main agent with Codex subagents still gets it on the subagents.
8
10
  const ENTRY = 'agent-watch-fast';
9
11
  export const NOTICE = 'agent-watch-fast-notice';
10
12
  export const FAST_SERVICE_TIER = 'priority';
@@ -27,7 +29,7 @@ export function fastSetter(owner, manager) {
27
29
  return enabled => {
28
30
  if (enabled && !fastAllowed(owner.manifest)) throw Error('managed-fast-unavailable');
29
31
  if (Boolean(enabled) === Boolean(owner.fast)) return false;
30
- owner.fast = Boolean(enabled); manager.appendCustomEntry(ENTRY, { enabled: owner.fast, allowed: fastAllowed(owner.manifest), at: new Date().toISOString() });
32
+ owner.fast = Boolean(enabled); owner.fastDeclined?.clear(); manager.appendCustomEntry(ENTRY, { enabled: owner.fast, allowed: fastAllowed(owner.manifest), at: new Date().toISOString() });
31
33
  return true;
32
34
  };
33
35
  }
@@ -44,21 +46,22 @@ export const fastServed = route => route?.provider === 'codex' && CODEX_FAST.has
44
46
  // Studio's refusal of the tier (the Harness or the model list changed), or
45
47
  // the provider's of a request carrying it: asked again once without it.
46
48
  export const fastRefusal = code => code === 'policy_denied' || code === 'upstream_request_rejected';
47
- // A fast request was refused before the model did any work: the conversation
48
- // goes back to standard speed and says so, instead of failing the turn.
49
- export function declineFast(owner, code) {
50
- owner.fast = false;
49
+ // A fast request was refused before the model did any work: that model goes
50
+ // back to standard speed and the others keep fast mode. Only the main agent's
51
+ // model says so; turning Fast off and on asks again.
52
+ export function declineFast(owner, code, role = 'main', route = null) {
53
+ (owner.fastDeclined ??= new Set()).add(route?.native ?? '');
54
+ if (role !== 'main') return;
51
55
  try {
52
- recordFast(owner, owner.session.sessionManager);
53
56
  void owner.session.sendCustomMessage?.({ customType: NOTICE, display: true, details: { code },
54
- content: `Fast mode was refused for this request [${code}]: this conversation continues at standard speed. Turn Fast on again to retry.` }, { triggerTurn: false })?.catch?.(() => {});
57
+ content: `Fast mode was refused for this model [${code}]: its calls continue at standard speed. Turn Fast off and on again to retry.` }, { triggerTurn: false })?.catch?.(() => {});
55
58
  } catch { /* the request goes on at standard speed either way */ }
56
59
  }
57
- // The payload hook of one request: the main agent's call on a model that
58
- // serves fast mode carries the tier while fast mode is on and still allowed
59
- // (the Harness may have turned it off since the member turned it on).
60
- export function fastPayload(owner, role, route, onPayload) {
61
- if (role !== 'main' || !owner.fast || !fastAllowed(owner.manifest) || !fastServed(route)) return onPayload;
60
+ // The payload hook of one request: a call on a model that serves fast mode
61
+ // carries the tier while fast mode is on and still allowed (the Harness may
62
+ // have turned it off since the member turned it on).
63
+ export function fastPayload(owner, route, onPayload) {
64
+ if (!owner.fast || !fastAllowed(owner.manifest) || !fastServed(route) || owner.fastDeclined?.has(route.native)) return onPayload;
62
65
  return async (payload, model) => {
63
66
  const next = (onPayload ? await onPayload(payload, model) : undefined) ?? payload;
64
67
  return next && typeof next === 'object' && !Array.isArray(next) ? { ...next, service_tier: FAST_SERVICE_TIER } : next;
@@ -87,7 +87,7 @@ export function countedCheck(managed, tool) {
87
87
  try { const result = await tool.execute(...args); ok = !result?.isError; return result; }
88
88
  finally {
89
89
  const after = counted && before !== null ? await folder() : null;
90
- if (after !== null && after === before) await managed.run.afterTool('bash', args[1], ok, () => managed.digest());
90
+ if (after !== null && after === before) await managed.run.afterTool('bash', args[1], ok, () => managed.digest(), managed);
91
91
  }
92
92
  } };
93
93
  }
@@ -4,6 +4,7 @@
4
4
  // refusal, starts once more: Studio then gives the new helper the next model
5
5
  // of its role that serves (an auto role's list).
6
6
  import { fenced, HELPER_UNTRUSTED_RULE } from './untrusted.mjs';
7
+ import { relevantChecks } from './check-evidence.mjs';
7
8
  import { jobTitle } from './call-purpose.mjs';
8
9
  import { ManagedToolBoundary } from './tool-boundary.mjs';
9
10
  import { managedResourceLoader } from './resource-loader.mjs';
@@ -144,7 +145,7 @@ async function answerUnder(managed, key, task, signal, harness, snapshot, parts,
144
145
  (managed.jobTitles ??= new WeakMap()).set(runtime, jobTitle(task, title));
145
146
  const setup = HELPER_SETUP[role], memory = helperMemoryTools(managed, () => grant, renew);
146
147
  boundary = new ManagedToolBoundary({ cwd: managed.cwd, sdkRoot: managed.sdk, protectedRoots: managed.protectedRoots, readOnly: true, commands: setup.commands });
147
- const checks = role === 'verify' ? `\n\nRepository checks: ${managed.checks.commands.length ? managed.checks.commands.map(c => '`' + c + '`').join(', ') : 'none declared or detected; choose the tests, type check or build that cover the claims'}.${managed.run?.fullAsked || managed.run?.scopeChoice === 'full' ? '' : ' Run only the tests that cover the claims: the member decides about a whole-suite run.'}` : '';
148
+ const checks = role === 'verify' ? `\n\nRepository checks: ${managed.checks.commands.length ? relevantChecks(managed, managed.checks.commands).map(c => '`' + c + '`').join(', ') : 'none declared or detected; choose the tests, type check or build that cover the claims'}.${managed.run?.fullAsked || managed.run?.scopeChoice === 'full' ? '' : ' Run only the tests that cover the claims: the member decides about a whole-suite run.'}` : '';
148
149
  // One helper session answers the brief (with one part of the patch for a
149
150
  // review in parts); its reply and tokens. A helper runs at its role's
150
151
  // level from Studio (fixed by the Harness, or its main agent's).
@@ -201,7 +202,7 @@ async function answerUnder(managed, key, task, signal, harness, snapshot, parts,
201
202
  for (const session of sessions) session.dispose(); await boundary?.dispose();
202
203
  await settleHelper(managed, renew.lease, report); // with the role's current token, before it closes
203
204
  await closeRole(managed, key);
204
- for (const who of watched) helperState(managed, who, null);
205
+ for (const who of watched) { helperState(managed, who, null); saveHelperSteps(managed, who); }
205
206
  }
206
207
  }
207
208
 
@@ -216,12 +217,29 @@ export function helperState(managed, key, state) {
216
217
  // latest tool step, told to listeners as it changes (company-terminal.mjs).
217
218
  function watchHelper(managed, key, session, task, title) {
218
219
  const job = String(title || task.split('\n').find(line => line.trim()) || '').trim().slice(0, 120);
220
+ const logs = managed.helperLogs ??= new Map();
221
+ const log = logs.get(key) ?? { job, startedAt: new Date().toISOString(), total: 0, steps: [] }; logs.set(key, log);
219
222
  helperState(managed, key, { job, step: 'đọc brief' });
220
223
  session.subscribe?.(event => {
221
224
  if (event?.type !== 'tool_execution_start') return;
222
- helperState(managed, key, { job, step: stepText(event.toolName, event.args) });
225
+ const args = stepArgs(event.args);
226
+ log.total += 1; log.steps.push({ tool: String(event.toolName), args, at: new Date().toISOString() }); log.steps.splice(0, log.steps.length - HELPER_STEPS_KEPT);
227
+ helperState(managed, key, { job, step: stepText(event.toolName, event.args), tool: String(event.toolName), args });
223
228
  });
224
229
  }
230
+ // A helper's session lives in memory: what it called is kept here so the
231
+ // dashboard's Activity can still show it once the helper has ended.
232
+ const HELPER_STEPS_KEPT = 200;
233
+ function saveHelperSteps(managed, key) {
234
+ const log = managed.helperLogs?.get(key); if (!log) return;
235
+ managed.helperLogs.delete(key);
236
+ managed.session?.sessionManager?.appendCustomEntry?.('agent-watch-helper-steps', { agent: key, ...log, endedAt: new Date().toISOString() });
237
+ }
238
+ function stepArgs(args = {}) {
239
+ const out = {};
240
+ for (const name of ['path', 'pattern', 'command', 'query', 'url']) if (typeof args?.[name] === 'string') out[name] = args[name].slice(0, 300);
241
+ return out;
242
+ }
225
243
  const STEP = { read: 'đọc', grep: 'tìm', find: 'tìm file', ls: 'xem thư mục', bash: 'chạy', web_search: 'tìm trên web', web_fetch: 'đọc trang', memory_search: 'tra team memory', memory_ask: 'hỏi team memory' };
226
244
  export function stepText(tool, args = {}) {
227
245
  const what = args?.path ?? args?.pattern ?? args?.command ?? args?.query ?? args?.url ?? '';
@@ -120,7 +120,7 @@ export function rejudgeBrief(item) {
120
120
  // One step of the completion gate (true: the gate goes round again). A
121
121
  // rejected premise objection goes back to its helper, which withdraws it or
122
122
  // keeps it for the member; then the items the main agent never decided are
123
- // put to it once, with no new review.
123
+ // put to it once each, with no new review: a later review's findings are new items.
124
124
  export async function settleLedger(managed, run, { note, send, signal }) {
125
125
  const rejected = toRejudge(run)[0];
126
126
  if (rejected) {
@@ -133,9 +133,9 @@ export async function settleLedger(managed, run, { note, send, signal }) {
133
133
  await note(`Harness: the ${rejected.role} subagent keeps its objection; the member decides.\n${itemText(rejected)}`, { phase: 'dispute', role: rejected.role, id: rejected.id });
134
134
  return true;
135
135
  }
136
- const pending = undecided(run);
137
- if (!pending.length || run.decideAsked) return false;
138
- run.decideAsked = true;
136
+ const asked = run.decidedAsked ??= new Set(), pending = undecided(run).filter(item => !asked.has(item.id));
137
+ if (!pending.length) return false;
138
+ for (const item of pending) asked.add(item.id);
139
139
  await send(decideText(pending), { phase: 'answer', ids: pending.map(i => i.id), issues: pending.slice(0, 10).map(i => ({ kind: i.kind, detail: i.detail })) });
140
140
  return true;
141
141
  }
@@ -151,7 +151,7 @@ export function ledgerWords(run) {
151
151
  const items = ledgerOf(run), count = s => items.filter(i => i.status === s).length, words = [];
152
152
  if (!items.length && !run.minorFindings) return null;
153
153
  for (const [status, label] of [['accepted', 'accepted'], ['rejected', 'rejected'], ['withdrawn', 'withdrawn by the helper'], ['cleared', 'cleared by a later check'], ['disputed', 'for the member'], ['open', 'left without a decision'], ['noted', 'noted']])
154
- if (count(status)) words.push(`${count(status)} ${label}`);
154
+ if (count(status)) words.push(`${count(status)} ${label}${status === 'open' ? ` (${items.filter(i => i.status === 'open').map(i => i.id).join(', ')})` : ''}`);
155
155
  if (run.minorFindings) words.push(`${run.minorFindings} minor finding(s) listed`);
156
156
  return `objections: ${words.join(', ')}`;
157
157
  }
@@ -55,7 +55,7 @@ export class CheckLessons {
55
55
  // shape it becomes a failure lesson for the team memory.
56
56
  #toolError(call,event,output) {
57
57
  if(!COMMAND_TOOLS.has(call.tool)||call.check||this.lessons.length>=MAX_LESSONS)return;
58
- if(/managed[-_](?:tool[-_]cancelled|operation[-_]denied|helper[-_]cancelled)|Command aborted|Command timed out/i.test(output))return;
58
+ if(/managed[-_](?:tool[-_]cancelled|operation[-_](?:denied|expired|cancelled)|helper[-_]cancelled)|Command aborted|Command timed out/i.test(output))return;
59
59
  const line=output.split('\n').find(l=>ERROR_LINE.test(l));
60
60
  if(!line)return;
61
61
  const command=commandShape(call.command);
@@ -85,7 +85,7 @@ export class CheckLessons {
85
85
  const output=textOf(event.result), c=call.check;
86
86
  const exitFailed=event.isError || (event.result?.details?.exitCode !== undefined && event.result.details.exitCode!==0);
87
87
  if(exitFailed || (c.piped && failedOutput(output))) {
88
- if(/managed[-_](?:tool[-_]cancelled|operation[-_]denied|helper[-_]cancelled)|Command aborted|Command timed out/i.test(output))return;
88
+ if(/managed[-_](?:tool[-_]cancelled|operation[-_](?:denied|expired|cancelled)|helper[-_]cancelled)|Command aborted|Command timed out/i.test(output))return;
89
89
  this.failed++;this.last='failed';
90
90
  const diagnostic=output.split('\n').find(l=>/error|fail|exception|not found|EADDRINUSE/i.test(l)) ?? 'Check exited unsuccessfully';
91
91
  if(this.failures.size>=20)this.failures.delete(this.failures.keys().next().value);
@@ -1,4 +1,5 @@
1
1
  import { broadAccess } from './permission.mjs';
2
+ import { approvalRefusal } from './approval-refusal.mjs';
2
3
  import fs from 'node:fs';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
@@ -121,7 +122,7 @@ export function fetchOriginTool(self) {
121
122
  reason:`Fetch origin branches from ${plan.repository}`,riskClass:'low',allowConsequence:'Download origin branches once; keep the current branch and working files unchanged.',denyConsequence:'No network request or credential read.'},
122
123
  terminalConfirm:()=>ctx?.ui?.confirm?.('Tải nhánh từ origin',`Tải các nhánh của ${plan.repository} về ${plan.cwd} một lần.\n\nĐồng ý: chỉ tải nhánh; nhánh hiện tại và file đang làm không đổi.\nTừ chối: không gửi request mạng, không đọc thông tin đăng nhập.`)??Promise.resolve(false),
123
124
  unavailableFallback:'terminal-confirm',recheck:()=>!signal?.aborted&&Boolean(self.grant)});
124
- if(!decision.allowed||!decision.consume()||signal?.aborted)throw Error('managed-operation-denied');
125
+ if(!decision.allowed||!decision.consume()||signal?.aborted)throw approvalRefusal(decision);
125
126
  return executeRepositoryFetch(self.boundary,plan,signal);
126
127
  }};
127
128
  }
@@ -92,7 +92,7 @@ export function wrapRoleStreams(owner, runtime, role, { provider, verifyGrant })
92
92
  // same company account and prompt cache; a helper names its role.
93
93
  const conversation = role === 'main' ? conversationSession(owner) : null;
94
94
  // Fast mode, where the member turned it on and the model serves it (fast-mode.mjs).
95
- const onPayload = fastPayload(owner, role, route, options?.onPayload), fast = onPayload !== options?.onPayload;
95
+ const onPayload = fastPayload(owner, route, options?.onPayload), fast = onPayload !== options?.onPayload;
96
96
  // Studio's id for this request, from its answer's headers: a
97
97
  // stream that ends here without an answer is ended there too.
98
98
  let studioRequest = null;
@@ -118,7 +118,7 @@ export function wrapRoleStreams(owner, runtime, role, { provider, verifyGrant })
118
118
  out.end(told(result)); return;
119
119
  }
120
120
  // Refused with the fast tier: asked again at standard speed.
121
- if (fast && fastRefusal(failureCode(refusal.errorMessage))) { letGo(); declineFast(owner, failureCode(refusal.errorMessage)); continue; }
121
+ if (fast && fastRefusal(failureCode(refusal.errorMessage))) { letGo(); declineFast(owner, failureCode(refusal.errorMessage), role, route); continue; }
122
122
  refused = String(refusal.errorMessage ?? '');
123
123
  // A lapsed or replaced grant: the next ask renews it, not this share.
124
124
  const stale = share; letGo();
@@ -482,10 +482,10 @@ export class ManagedSession {
482
482
  publishHelpers() {
483
483
  // One of each enabled helper role can run at a time.
484
484
  const maximum = helperRoles(this.manifest).length;
485
- this.session.sessionManager.appendCustomEntry('agent-watch-helpers', {active:this.helpers.size,maximum});
485
+ this.session.sessionManager.appendCustomEntry('agent-watch-helpers', {active:this.helpers.size,maximum,roles:[...this.helpers.keys()]});
486
486
  // A helper runs while the main agent is quiet: listeners (the WebUI's
487
487
  // "1/4 running") are told, since no agent event would say so.
488
- this.notify({ type: 'managed_helpers', active: this.helpers.size, maximum });
488
+ this.notify({ type: 'managed_helpers', active: this.helpers.size, maximum, roles: [...this.helpers.keys()] });
489
489
  }
490
490
  notify(event) {
491
491
  for (const listener of [...this.listeners]) { try { listener(event); } catch { /* a listener's failure is its own */ } }
@@ -112,7 +112,7 @@ ${proxyPort && !network ? `(allow mach-lookup (global-name "com.apple.trustd.age
112
112
  (allow network-outbound (literal ${literal(docker)}))` : ''}` : loopback ? '(allow network-outbound (remote ip "localhost:*"))' : proxyPort ? `(allow network-outbound (remote ip "localhost:${Number(proxyPort)}"))` : ''}
113
113
  ${denied.map(root => `(deny file-read* file-write* (require-all (subpath ${literal(root)})${skills.filter(dir => inside(dir, root) && dir !== root).map(dir => ` (require-not (subpath ${literal(dir)}))`).join('')}))`).join('\n')}
114
114
  (deny file-read* file-read-metadata file-write* (require-all (require-any (regex #"(^|/)(\\.env([./]|$)|credentials([./]|$)|\\.npmrc$|\\.netrc$)") (regex #"/\\.[^/]+/auth\\.json$") ${projectRoots.map(root => `(literal ${literal(path.join(root, 'auth.json'))})`).join(' ')})
115
- (require-not (regex #"(^|/)\\.env\\.(example|sample|template|dist|defaults)$"))))
115
+ (require-not (regex #"(^|/)\\.env\\.(example|sample|template|dist|defaults)$")))) (allow file-read* file-read-metadata file-write* (require-all (subpath ${literal(temporary)}) (regex #"/\\.env([.][^/]*)?$")))
116
116
  `;
117
117
  }
118
118
  // Toolchains from the pinned node/git and Homebrew, compilers and SDK; the
@@ -15,7 +15,7 @@ const INPUT_CHARS = 500, ERROR_HEAD = 1000, ERROR_TAIL = 2000;
15
15
  // One request carries at most BATCH failures; KEEP wait for the next one.
16
16
  const BATCH = 20, KEEP = 100, ATTEMPTS = 3;
17
17
  // The member stopped the tool or declined to approve it: not a failure.
18
- const NOT_FAILURES = new Set(['managed_tool_cancelled', 'managed_helper_cancelled', 'managed_operation_denied']);
18
+ const NOT_FAILURES = new Set(['managed_tool_cancelled', 'managed_helper_cancelled', 'managed_operation_denied', 'managed_operation_expired', 'managed_operation_cancelled']);
19
19
 
20
20
  const text = result => typeof result === 'string' ? result
21
21
  : Array.isArray(result?.content) ? result.content.filter(part => part?.type === 'text').map(part => part.text ?? '').join('\n') : '';
@@ -4,7 +4,8 @@ import { TsDiagnostics } from './ts-diagnostics.mjs';
4
4
  import { checkpointsOf } from './checkpoints.mjs';
5
5
  import { declaredChecks, detectedChecks, repositoryChecks, isCheckCommand, isReadOnlyCommand, nonCodePath, onlyNonCode } from './repository-checks.mjs';
6
6
  export { declaredChecks, detectedChecks, repositoryChecks, isCheckCommand, isReadOnlyCommand, nonCodePath, onlyNonCode };
7
- import { askCheckScope, asksFullChecks, beforeCheckOrPush, checkScope } from './check-questions.mjs';
7
+ import { askCheckScope, asksFullChecks, beforeCheckOrPush, checkScope, suitePart } from './check-questions.mjs';
8
+ import { recordCheck, relevantChecks } from './check-evidence.mjs';
8
9
  import { LANE_LIMITS, patchStats, changeLane, lanePolicy } from './risk-lanes.mjs';
9
10
  import { briefCounts, decisionsText, ledgerGuard, ledgerOf, ledgerWords, reviewFocus, saveLedger, setStatus, settleLedger } from './ledger.mjs';
10
11
 
@@ -81,7 +82,7 @@ export function workflowPrompt(policy, checks) {
81
82
  const L = LANE_LIMITS;
82
83
  lines.push(`- Size: the process follows the size of your change. A small change (at most ${L.tinyFiles} files and ${L.tinyLines} changed lines, no migration, auth, security, CI, dependency or AGENTS.md file) ends after its checks, without a review: do not ask for one. A risky change (a complex request, one of those files, more than ${L.riskyFiles} files or ${L.riskyLines} lines) must pass the checks${policy.review !== 'off' ? ' and the harness review' : ''} even where they are only suggested.`);
83
84
  }
84
- return lines.length ? `\n\nProcess for code changes (company Harness):\n${lines.join('\n')}\n${process.platform === 'darwin' ? 'End-to-end and UI checks run in bash with the project\'s Playwright (@playwright/test; add it with npm when the project lacks it) and the browsers installed on this Mac, against a server the test or start_process starts; while a local proxy listens they need run_with_network. When Playwright\'s browsers are missing, ask with run_on_host to run `npx playwright install chromium`.' : 'End-to-end tests that start a local server and a browser run through run_with_network, which the user approves.'} If a check cannot run because the machine lacks something (a runtime such as a JDK, Docker, a database or another service, the network), do not stop there: request the approval that makes it run (run_with_network, run_with_docker, start_docker, or run_on_host to install what is missing) or ask the member with ask_user; never install or start anything without that approval.\nA command stops after 10 minutes and anything left running in the background stops when the turn ends: run the checks that cover your change first and a long suite in parts, not in the background.${process.platform === 'darwin' ? ' A dev server, watcher or local service you need across turns runs with start_process (read it with process_output, stop it with stop_process).' : ''}` : '';
85
+ return lines.length ? `\n\nProcess for code changes (company Harness):\n${lines.join('\n')}\n${process.platform === 'darwin' ? 'A bash command stops after 10 minutes unless you pass timeout in seconds, at most 1800: a long suite needs that, not another tool. End-to-end and UI checks run in bash with the project\'s Playwright (@playwright/test; add it with npm when the project lacks it) and the browsers installed on this Mac, against a server the test or start_process starts; while a local proxy listens they need run_with_network. When Playwright\'s browsers are missing, ask with run_on_host to run `npx playwright install chromium`.' : 'End-to-end tests that start a local server and a browser run through run_with_network, which the user approves.'} If a check cannot run because the machine lacks something (a runtime such as a JDK, Docker, a database or another service, the network), do not stop there: request the approval that makes it run (run_with_network, run_with_docker, start_docker, or run_on_host to install what is missing) or ask the member with ask_user; never install or start anything without that approval.\nA command stops after 10 minutes and anything left running in the background stops when the turn ends: run the checks that cover your change first and a long suite in parts, not in the background.${process.platform === 'darwin' ? ' A dev server, watcher or local service you need across turns runs with start_process (read it with process_output, stop it with stop_process).' : ''}` : '';
85
86
  }
86
87
 
87
88
  // Every helper checks the brief it was given: it may object with evidence, it
@@ -173,7 +174,7 @@ export class RunProcess {
173
174
  // asked for, and where the harness last sent the agent back (to read
174
175
  // its answer and see whether the code moved).
175
176
  ledger: [], disputes: [], verifyFails: [], claimsAsked: false, claimsAt: null, claimsRechecked: false, claimsDone: false, mainReviews: 0, minorFindings: 0,
176
- sentAt: null, sentDigest: null, rejudged: false, reviewSettled: false, decideAsked: false,
177
+ sentAt: null, sentDigest: null, rejudged: false, reviewSettled: false,
177
178
  // How much of the suite the member wants run (check-questions.mjs).
178
179
  scopeChoice: null, scopeWords: null, fullAsked: asksFullChecks(request), pushAllowedOn: undefined,
179
180
  // The patch's risk lane (workflow.lanes) and the main agent's own web lookups.
@@ -197,15 +198,17 @@ export class RunProcess {
197
198
  changedWithoutPlan() {
198
199
  this.unplannedChange = true; this.planSkipped = true;
199
200
  }
200
- async afterTool(name, args, ok, digestOf) {
201
+ // `owner`: the conversation the command ran in, for what passed in which repository.
202
+ async afterTool(name, args, ok, digestOf, owner = null) {
201
203
  if (['write', 'edit'].includes(name) && ok) this.edited = true;
202
204
  if (name === 'bash' && isCheckCommand(args?.command, this.checks.commands)) {
203
205
  const scope = checkScope(args.command, this.checks.commands), digest = await digestOf(), last = this.lastCheck;
204
206
  this.checksRun += 1; if (!ok) this.checksFailed += 1;
205
207
  // A lint after a partial test run does not make it the whole suite.
206
208
  this.lastCheck = { ok, digest, mutations: this.mutations, scope: scope ?? (last?.scope === 'partial' && last.digest === digest ? 'partial' : 'other') };
207
- // The member's "run it all" covers one run; after a fix they are asked again.
208
- if (scope === 'suite' && this.scopeChoice === 'full') this.scopeChoice = null;
209
+ await recordCheck(this, owner, args.command, scope, ok, suitePart(args.command));
210
+ // The member's "run it all" covers one run in each repository a push needs; after a failure they are asked again.
211
+ if (scope === 'suite' && this.scopeChoice === 'full' && (!ok || !this.fullFor?.size)) { this.scopeChoice = null; this.fullFor = null; }
209
212
  // A command may change files, so it counts. Without a git repository to
210
213
  // compare the code against, one that only reads does not: counting every
211
214
  // command reported code changes in turns that only read and checked.
@@ -298,7 +301,7 @@ export async function completionGate(managed, run, signal) {
298
301
  }
299
302
  if (policy.verify === 'require' && (!run.verified || run.scopeWords) && !run.docsOnly && !verifyGaveUp && run.waivedDigest !== digest) {
300
303
  run.fixLoops += 1;
301
- const list = run.checks.commands.length ? run.checks.commands.map(c => '`' + c + '`').join(', ') : 'the tests, type check or build that cover the change';
304
+ const list = run.checks.commands.length ? relevantChecks(managed, run.checks.commands).map(c => '`' + c + '`').join(', ') : 'the tests, type check or build that cover the change';
302
305
  const words = run.scopeWords; run.scopeWords = null;
303
306
  const ask = words ? `The member answered about testing: "${String(words).slice(0, 500)}". Follow that and fix what fails.`
304
307
  : run.scopeChoice === 'partial' ? 'Run only the tests that cover your change (one file, a filter, one package), not the whole suite, and fix what fails.'
@@ -314,6 +317,9 @@ export async function completionGate(managed, run, signal) {
314
317
  const focus = run.sentAt !== null ? await reviewFocus(managed) : null, previous = (review?.findings ?? []).filter(f => f.severity === 'blocking').map((f, i) => `${i + 1}. ${findingText(f)}`).join('\n');
315
318
  const brief = `Review the current patch against the member's request.\n\nChecks: ${run.partOnlyOn(digest) ? 'only part of the test suite passed on this code' : run.verified ? 'a repository check passed on this code' : 'no check has passed on this code'}.${answer ? `\n\nThe main agent's answer to your previous blocking findings (the code changed after it):\n${answer}` : ''}${focus ? `\n\nSince your last review only these files changed: ${focus.files.join(', ')}. Judge whether each previous blocking finding is fixed (repeat it when it is not) and review the changes in these files; the rest of the patch was reviewed and has not changed.${previous ? `\nYour previous blocking findings:\n${previous}` : ''}` : ''}`;
316
319
  managed.reviewFocus = focus;
320
+ // The answer so far is not the result yet: the dashboard marks it so.
321
+ // An entry, not a message: the model has nothing to do with it.
322
+ session.sessionManager.appendCustomEntry('agent-watch-review-start', {});
317
323
  try { await managed.delegate({ role: 'review', task: brief }, signal, { harness: true }); }
318
324
  catch (error) {
319
325
  run.reviewUnavailable = !stopped(); run.reviewUnavailableReason = reviewUnavailableReason(error?.message); interrupted = stopped();
@@ -443,7 +449,7 @@ export function processEditTools(session, tools) {
443
449
  }
444
450
  return result;
445
451
  }
446
- finally { await session.run?.afterTool(tool.name, args[1], ok, () => session.digest()); await session.refreshReview(); }
452
+ finally { await session.run?.afterTool(tool.name, args[1], ok, () => session.digest(), session); await session.refreshReview(); }
447
453
  } };
448
454
  });
449
455
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/core",
3
- "version": "1.49.0",
3
+ "version": "1.50.0",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -8,7 +8,7 @@ const PUBLIC_REF = /^[A-Za-z0-9][A-Za-z0-9._:@~-]{0,159}$/;
8
8
  const REVISION = /^[A-Za-z0-9][A-Za-z0-9._:~-]{0,159}$/;
9
9
  const DIGEST = /^sha256:[a-f0-9]{64}$/;
10
10
  const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
11
- const MAX_PENDING = 32, MAX_RECENT = 64, DEFAULT_TTL_MS = 5 * 60_000;
11
+ const MAX_PENDING = 32, MAX_RECENT = 64, DEFAULT_TTL_MS = 5 * 60_000, MAX_TTL_MS = 30 * 60_000;
12
12
 
13
13
  type Identity = { projectRef: string; runtimeInstanceId: string; sessionRef: string; taskId: string; taskRunId: string;
14
14
  agentOperationId: string; toolCallId: string };
@@ -161,7 +161,7 @@ export class PiApprovalBroker {
161
161
  if (!binding || !authority || authority.taskState !== "active" || input.expectedTask && (authority.identity.taskId !== input.expectedTask.taskId || authority.identity.taskRunId !== input.expectedTask.taskRunId)
162
162
  || !REF.test(authority.identity.agentOperationId)
163
163
  || !toolCallId || binding.pending.size >= MAX_PENDING) return this.#unavailable(input);
164
- const now = Date.now(), requestedAt = new Date(now).toISOString(), expiresAt = new Date(now + Math.min(Math.max(input.ttlMs ?? DEFAULT_TTL_MS, 5_000), DEFAULT_TTL_MS)).toISOString();
164
+ const now = Date.now(), requestedAt = new Date(now).toISOString(), expiresAt = new Date(now + Math.min(Math.max(input.ttlMs ?? DEFAULT_TTL_MS, 5_000), MAX_TTL_MS)).toISOString();
165
165
  const approvalRef = ref("approval", binding.secret, [toolCallId, now, randomUUID()]);
166
166
  const decisionToken = randomBytes(32).toString("base64url");
167
167
  const nextRevision = this.#nextRevision(binding, "requested", approvalRef);
@@ -0,0 +1 @@
1
+ import{n as e,r as t}from"./rolldown-runtime-hePW80VL.js";import{An as n,Rn as r}from"./vendor-DNZbaWhT.js";import{i,n as a}from"./ui-preferences-OfS30-ZV.js";import{B as o,K as s,x as c}from"./api-BQS4Dtrr.js";import{c as l}from"./view-model-CWNiS4JN.js";var u=t(r(),1);function d(e,t=`vi`){let n=e.state===`running`?`running`:e.state===`failed`?`failed`:`passed`,r=n===`running`?t===`vi`?`đang chạy`:`running`:n===`failed`?t===`vi`?`thất bại`:`failed`:t===`vi`?`hoàn tất`:`completed`,i=e.startedAt??e.finishedAt??``;return{activityRef:`live.${e.toolCallRef}`,kind:[`bash`,`shell`,`exec`].includes(e.toolLabel)?`command`:`tool`,state:n,label:`${e.agent?`${e.agent} · `:``}${e.toolLabel} ${r}`,preview:e.detail??e.fileLabel??(t===`vi`?`Cập nhật trực tiếp từ Gateway`:`Live update from the Gateway`),toolCallId:e.toolCallRef,toolName:e.toolLabel,commandDigest:null,logRef:null,exitCode:null,exitCodeExact:!1,startedAt:i,finishedAt:n===`running`?null:e.finishedAt??i}}function f(e,t=`vi`){let n=e.state===`failed`?t===`vi`?`Lượt chạy gặp lỗi`:`Operation failed`:e.state===`blocked`?t===`vi`?`Lượt chạy bị chặn`:`Operation blocked`:e.state===`aborted`?t===`vi`?`Lượt chạy đã dừng`:`Operation stopped`:t===`vi`?`Chưa xác định kết quả lượt chạy`:`Operation outcome unknown`;return{activityRef:e.activityRef,kind:`system`,state:e.state,label:n,preview:e.reasonCode,toolCallId:null,toolName:null,commandDigest:null,logRef:null,exitCode:null,exitCodeExact:!1,startedAt:e.settledAt,finishedAt:e.settledAt}}function p(e,t,n,r=`vi`,i){let a=[...e,...t],o=new Set(a.map(e=>e.activityRef)),s=n.filter(e=>!o.has(e.activityRef)).map(e=>f(e,r));if(i===void 0)return{rows:[...e,...s,...t],terminalCount:s.length,runningCount:e.length};let c=i.filter(e=>e.state===`running`).map(e=>d(e,r));return{rows:[...c,...s,...t],terminalCount:s.length,runningCount:c.length}}function m(e,t=`vi`){return e.state===`passed`&&/ recovered$/i.test(e.label)?t===`vi`?`Đã khôi phục`:`Recovered`:e.state===`passed`&&/ warning$/i.test(e.label)?t===`vi`?`Đã xử lý cảnh báo`:`Handled warning`:e.state===`passed`&&e.exitCodeExact?`Pass · exit ${e.exitCode??0}`:e.state===`failed`&&e.exitCodeExact?`Fail · exit ${e.exitCode??`?`}`:e.state===`blocked`?`Blocked`:e.state===`aborted`?t===`vi`?`Đã dừng`:`Stopped`:e.state===`unknown`?t===`vi`?`Chưa xác định`:`Unknown`:e.state===`failed`?t===`vi`?`Thất bại`:`Failed`:e.state===`running`?t===`vi`?`Đang chạy`:`Running`:e.state.replaceAll(`-`,` `)}function h(e,t=`vi`){let n=Date.parse(e.startedAt),r=e.finishedAt?Date.parse(e.finishedAt):Date.now();if(!Number.isFinite(n)||!Number.isFinite(r)||r<n)return t===`vi`?`Thời gian chưa xác định`:`Unknown duration`;let i=Math.round((r-n)/1e3);return i<60?`${i}s`:`${Math.floor(i/60)}m ${i%60}s`}function g(e,t=[]){let n=new Map([[`main`,{agent:`main`,steps:[],total:0}]]),r=e=>{let t=n.get(e);if(t)return t;let r={agent:e,steps:[],total:0};return n.set(e,r),r};for(let t of e){if(t.helperRun){let e=r(t.helperRun.agent);e.total+=t.helperRun.total,e.steps.push(...t.helperRun.steps.map((e,n)=>({key:`${t.messageRef}:${n}`,toolName:e.toolName,target:e.target,state:`completed`,at:e.at})));continue}if(t.role!==`assistant`)continue;let e=r(`main`);for(let n of t.toolCalls)e.total+=1,e.steps.push({key:`${t.messageRef}:${n.toolCallRef}`,toolName:n.toolName,target:n.summary?.target??n.summary?.detail??null,state:n.state===`failed`||n.result?.isError?`failed`:`completed`,at:t.recordedAt})}for(let e of t){if(!e.agent||e.state===`running`)continue;let t=r(e.agent);t.total+=1,t.steps.push({key:e.toolCallRef,toolName:e.toolLabel,target:e.detail??null,state:e.state,at:e.startedAt??``})}for(let e of n.values())e.steps.sort((e,t)=>t.at.localeCompare(e.at));return[...n.values()].filter(e=>e.steps.length>0)}var _=n();function v(e,t){return e===`main`?a(t,`Main agent`,`Main agent`):`Subagent ${e}`}function y(e,t){let n=new Date(e);return Number.isFinite(n.getTime())?n.toLocaleTimeString(t===`vi`?`vi-VN`:`en-US`,{hour:`2-digit`,minute:`2-digit`,second:`2-digit`}):``}var b=5e3;function x({sessionRef:e,running:t,live:n,locale:r,empty:i}){let[o,c]=(0,u.useState)([]);(0,u.useEffect)(()=>{let n=new AbortController,r=()=>void s(e,null,200,n.signal).then(e=>c(e.items)).catch(()=>void 0);r();let i=t?window.setInterval(r,b):void 0;return()=>{n.abort(),i&&window.clearInterval(i)}},[e,t]);let d=(0,u.useMemo)(()=>g(o,n),[o,n]);return d.length?(0,_.jsx)(`div`,{className:`agent-activity-log`,children:d.map(e=>(0,_.jsxs)(`details`,{open:e.agent===`main`||t,className:`agent-activity-group`,children:[(0,_.jsxs)(`summary`,{children:[(0,_.jsx)(`strong`,{children:v(e.agent,r)}),` `,(0,_.jsxs)(`span`,{children:[e.total,` `,a(r,`lần gọi`,`calls`)]}),e.total>e.steps.length&&(0,_.jsxs)(`span`,{children:[` · `,a(r,`hiện ${e.steps.length} gần nhất`,`showing the latest ${e.steps.length}`)]})]}),e.steps.map(e=>(0,_.jsxs)(`article`,{className:`activity-row`,children:[(0,_.jsx)(`span`,{className:`activity-state tone-${l(e.state)}`,"aria-label":e.state}),(0,_.jsxs)(`div`,{className:`activity-copy`,children:[(0,_.jsx)(`div`,{children:(0,_.jsx)(`strong`,{children:e.toolName})}),e.target&&(0,_.jsx)(`p`,{children:e.target})]}),(0,_.jsx)(`div`,{className:`activity-result`,children:(0,_.jsx)(`span`,{children:y(e.at,r)})})]},e.key))]},e.agent))}):i?(0,_.jsx)(`div`,{className:`activity-empty`,children:i}):null}var S=e({ActivityPanel:()=>w});function C({activity:e,preview:t,onPreview:n,locale:r}){return(0,_.jsxs)(`article`,{className:`activity-row`,children:[(0,_.jsx)(`span`,{className:`activity-state tone-${l(e.state)}`,"aria-label":e.state}),(0,_.jsxs)(`div`,{className:`activity-copy`,children:[(0,_.jsxs)(`div`,{children:[(0,_.jsx)(`strong`,{children:e.label}),(0,_.jsx)(`span`,{children:e.kind})]}),(0,_.jsx)(`p`,{children:e.preview||a(r,`Không có mô tả ngắn`,`No short description`)}),t&&(0,_.jsxs)(`div`,{className:`log-preview ${t.state===`error`?`error-state`:``}`,children:[t.state===`loading`?a(r,`Đang tải log ngắn…`,`Loading log preview…`):t.state===`error`?a(r,`Log preview không còn sẵn sàng.`,`Log preview is no longer available.`):t.value?.preview??`${a(r,`Không có nội dung log`,`No log content`)} · ${t.value?.reasonCode??`unavailable`}`,t.value?.truncated&&(0,_.jsx)(`small`,{children:a(r,`Preview đã được rút gọn`,`Preview truncated`)})]})]}),(0,_.jsxs)(`div`,{className:`activity-result`,children:[(0,_.jsx)(`strong`,{children:m(e,r)}),(0,_.jsx)(`span`,{children:h(e,r)}),e.logRef&&n&&(0,_.jsx)(`button`,{type:`button`,onClick:n,"aria-expanded":!!t,children:t?a(r,`Ẩn log`,`Hide log`):a(r,`Xem log`,`View log`)})]})]})}function w({snapshot:e,sessionRef:t,terminalActivities:n=[],liveActivities:r}){let{locale:s}=i(),[l,d]=(0,u.useState)({}),f=e?.activity.running??[],m=e?.activity.recent??[],{rows:h,terminalCount:g,runningCount:v}=p(f,m,n,s,r),y=e=>{if(l[e.activityRef]){d(t=>({...t,[e.activityRef]:void 0}));return}d(t=>({...t,[e.activityRef]:{state:`loading`}})),(t?o(t,e.activityRef):c(e.activityRef)).then(t=>d(n=>({...n,[e.activityRef]:{state:`ready`,value:t}}))).catch(()=>d(t=>({...t,[e.activityRef]:{state:`error`}})))};return(0,_.jsxs)(`section`,{className:`activity-panel surface`,"aria-labelledby":`activity-title`,children:[(0,_.jsxs)(`header`,{className:`panel-heading`,children:[(0,_.jsxs)(`div`,{children:[(0,_.jsx)(`p`,{className:`section-kicker`,children:`Activity`}),(0,_.jsx)(`h2`,{id:`activity-title`,children:a(s,`Tool, command và kết quả lượt chạy`,`Tools, commands, and operation outcomes`)})]}),(0,_.jsxs)(`div`,{children:[(0,_.jsxs)(`span`,{className:`running-count`,children:[v,` `,a(s,`đang chạy`,`running`)]}),(0,_.jsxs)(`span`,{children:[m.length+g,` `,a(s,`gần đây`,`recent`)]})]})]}),(0,_.jsxs)(`div`,{className:`activity-list`,children:[h.map(e=>(0,_.jsx)(C,{activity:e,preview:l[e.activityRef],onPreview:e.logRef?()=>y(e):void 0,locale:s},e.activityRef)),t&&(0,_.jsx)(x,{sessionRef:t,running:!!r,live:r,locale:s,empty:h.length===0?a(s,`Chưa có tool, command hoặc kết quả lượt chạy nào trong timeline hiện tại.`,`No tools, commands, or operation outcomes in the current timeline.`):void 0}),h.length===0&&!t&&(0,_.jsx)(`div`,{className:`activity-empty`,children:a(s,`Chưa có tool, command hoặc kết quả lượt chạy nào trong timeline hiện tại.`,`No tools, commands, or operation outcomes in the current timeline.`)})]}),e?.activity.page.truncated&&(0,_.jsx)(`footer`,{className:`panel-footnote`,children:a(s,`Timeline đã được rút gọn theo giới hạn local.`,`The timeline was truncated to local limits.`)})]})}export{S as n,w as t};