@piagent/platform 1.46.0 → 1.46.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 (72) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/docs/command-reference-vietnamese.md +2 -2
  3. package/docs/distribution-standard.md +6 -6
  4. package/docs/en/architecture.md +1 -1
  5. package/docs/harness-conformance.md +22 -5
  6. package/docs/languages.json +2 -2
  7. package/docs/operator-manual-vietnamese.md +7 -7
  8. package/docs/publishing-for-teams.md +1 -0
  9. package/docs/release-install-policy.md +2 -2
  10. package/docs/team-onboarding.md +2 -2
  11. package/docs/vi/architecture.md +1 -1
  12. package/docs-site/architecture.html +1 -1
  13. package/docs-site/assets/search-en.json +1 -1
  14. package/docs-site/assets/search-vi.json +1 -1
  15. package/docs-site/benchmark.html +1 -1
  16. package/docs-site/commands.html +1 -1
  17. package/docs-site/context-engine.html +1 -1
  18. package/docs-site/ecosystem.html +1 -1
  19. package/docs-site/en/architecture.html +1 -1
  20. package/docs-site/en/benchmark.html +1 -1
  21. package/docs-site/en/commands.html +1 -1
  22. package/docs-site/en/context-engine.html +1 -1
  23. package/docs-site/en/ecosystem.html +1 -1
  24. package/docs-site/en/index.html +9 -9
  25. package/docs-site/en/mcp-approval.html +1 -1
  26. package/docs-site/en/mcp-auth.html +1 -1
  27. package/docs-site/en/mcp-servers.html +1 -1
  28. package/docs-site/en/mcp.html +1 -1
  29. package/docs-site/en/permissions.html +1 -1
  30. package/docs-site/en/profiles.html +1 -1
  31. package/docs-site/en/quickstart.html +1 -1
  32. package/docs-site/en/runtime.html +1 -1
  33. package/docs-site/en/scope.html +1 -1
  34. package/docs-site/en/security.html +1 -1
  35. package/docs-site/en/team.html +1 -1
  36. package/docs-site/en/update.html +3 -3
  37. package/docs-site/en/whats-new.html +27 -13
  38. package/docs-site/en/windows.html +1 -1
  39. package/docs-site/en/workflows.html +1 -1
  40. package/docs-site/index.html +9 -9
  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/releases.json +44 -0
  49. package/docs-site/runtime.html +1 -1
  50. package/docs-site/scope.html +1 -1
  51. package/docs-site/security.html +1 -1
  52. package/docs-site/team.html +1 -1
  53. package/docs-site/update.html +3 -3
  54. package/docs-site/whats-new.html +27 -13
  55. package/docs-site/windows.html +1 -1
  56. package/docs-site/workflows.html +1 -1
  57. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  58. package/package.json +1 -1
  59. package/packages/piagent-core/managed/copies.mjs +61 -0
  60. package/packages/piagent-core/managed/grant-share.mjs +9 -1
  61. package/packages/piagent-core/managed/helper-roles.mjs +5 -3
  62. package/packages/piagent-core/managed/helper-run.mjs +120 -52
  63. package/packages/piagent-core/managed/helper-settlement.mjs +5 -1
  64. package/packages/piagent-core/managed/ledger.mjs +187 -0
  65. package/packages/piagent-core/managed/objections.mjs +49 -27
  66. package/packages/piagent-core/managed/patch-snapshot.mjs +9 -0
  67. package/packages/piagent-core/managed/session.mjs +14 -14
  68. package/packages/piagent-core/managed/toolchain.mjs +7 -3
  69. package/packages/piagent-core/managed/workflow.mjs +32 -26
  70. package/packages/piagent-core/package.json +1 -1
  71. package/packages/piagent-core/runtime/managed-failure.mjs +8 -0
  72. package/scripts/company-terminal.mjs +19 -1
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.46.0" title="v1.46.0 docs · VI"><span class="dot"></span>v1.46.0</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.46.1" title="v1.46.1 docs · VI"><span class="dot"></span>v1.46.1</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new" aria-current="page">Changelog</a>
@@ -108,7 +108,22 @@
108
108
  <div class="release-filter" role="toolbar" aria-label="Lọc theo loại thay đổi"><button type="button" data-filter-kind="all" aria-pressed="true">Tất cả</button><button type="button" data-filter-kind="new" aria-pressed="false">Mới</button><button type="button" data-filter-kind="improved" aria-pressed="false">Cải thiện</button><button type="button" data-filter-kind="fixed" aria-pressed="false">Sửa lỗi</button><button type="button" data-filter-kind="security" aria-pressed="false">Bảo mật</button></div>
109
109
  <section class="section releases" data-releases>
110
110
  <h2 id="latest" class="sr-only">Mới nhất</h2>
111
- <article class="release latest" id="v1-46-0" data-release>
111
+ <article class="release latest" id="v1-46-1" data-release>
112
+ <div class="release-meta"><a class="release-version" href="#v1-46-1">v1.46.1</a><time class="release-date" datetime="2026-10-28">28/10/2026</time></div>
113
+ <div class="release-body">
114
+ <h3 data-toc-skip id="v1-46-1-title">Phản biện có sổ ghi, review một nhịp, subagent song song khi được yêu cầu</h3>
115
+ <p>Harness công ty ghi một sổ phản biện cùng quyết định của main, review một nhịp mỗi lượt, và chỉ chạy 2–3 helper cùng role song song khi member yêu cầu.</p>
116
+ <ul class="release-items">
117
+ <li data-kind="new"><span class="kind kind-new">Mới</span><span>Phản biện brief, finding review và claim fail chung một sổ; main ghi quyết định bằng resolve_objection, phản biện sai tiền đề chặn sửa file đến khi được quyết.</span></li>
118
+ <li data-kind="new"><span class="kind kind-new">Mới</span><span>Mặc định mỗi role một helper; khi member yêu cầu 2–3 helper cùng role, chúng chạy song song, mỗi helper một grant với Agent Studio 0.50.0 và Agent Watch 0.15.17.</span></li>
119
+ <li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Review một nhịp mỗi lượt: review bắt buộc do harness làm, review lại sau khi sửa chỉ đọc file đã đổi, finding minor chỉ liệt kê.</span></li>
120
+ <li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Terminal công ty hiện checklist và việc từng subagent đang làm ngay trên ô nhập.</span></li>
121
+ <li data-kind="fixed"><span class="kind kind-fixed">Sửa lỗi</span><span>piagent studio cài fd ở lần mở đầu tiên nên gợi ý @ hiện file và thư mục; trước đây fd không bao giờ được tải, kể cả khi đã có ở /opt/homebrew/bin.</span></li>
122
+ </ul>
123
+ <p class="release-requires"><strong>Cần:</strong> Agent Studio 0.50.0 và Agent Watch 0.15.17 (Windows 0.1.18) để subagent chạy song song</p>
124
+ </div>
125
+ </article>
126
+ <article class="release" id="v1-46-0" data-release>
112
127
  <div class="release-meta"><a class="release-version" href="#v1-46-0">v1.46.0</a><time class="release-date" datetime="2026-10-28">28/10/2026</time></div>
113
128
  <div class="release-body">
114
129
  <h3 data-toc-skip id="v1-46-0-title">Piagent chạy trong Pi home riêng, pi của bạn giữ nguyên bản gốc</h3>
@@ -411,22 +426,21 @@
411
426
  </ul>
412
427
  </div>
413
428
  </article>
414
- <article class="release" id="v1-38-1" data-release>
415
- <div class="release-meta"><a class="release-version" href="#v1-38-1">v1.38.1</a><time class="release-date" datetime="2026-10-23">23/10/2026</time></div>
416
- <div class="release-body">
417
- <h3 data-toc-skip id="v1-38-1-title">Bản phát hành chính thức của v1.38.0</h3>
418
- <p>v1.38.0 không qua release check vì hai test nên chưa từng được phát hành; v1.38.1 là cùng bản đó với hai test đã được sửa.</p>
419
- <ul class="release-items">
420
- <li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Hai test phát hành nay pass trên Linux và máy chạy chậm, nên editor, terminal, review, checkpoint và Bypass của v1.38.0 có trong bản này.</span></li>
421
- </ul>
422
- </div>
423
- </article>
424
429
  </section>
425
430
  <section class="section archive">
426
431
  <h2 id="archive">Các bản trước</h2>
427
432
  <p class="section-lead">Bản cũ hơn, gom theo tháng. Mở một tháng để xem từng thay đổi.</p>
428
433
  <details class="release-archive">
429
- <summary><span>Tháng 10/2026</span><span class="archive-count">57</span></summary>
434
+ <summary><span>Tháng 10/2026</span><span class="archive-count">58</span></summary>
435
+ <div class="archive-row" id="v1-38-1">
436
+ <div class="archive-head"><a class="release-version" href="#v1-38-1">v1.38.1</a> <span class="release-date">23/10/2026</span></div>
437
+ <div class="archive-body">
438
+ <p><strong>Bản phát hành chính thức của v1.38.0</strong> — v1.38.0 không qua release check vì hai test nên chưa từng được phát hành; v1.38.1 là cùng bản đó với hai test đã được sửa.</p>
439
+ <ul class="release-items">
440
+ <li data-kind="improved"><span class="kind kind-improved">Cải thiện</span><span>Hai test phát hành nay pass trên Linux và máy chạy chậm, nên editor, terminal, review, checkpoint và Bypass của v1.38.0 có trong bản này.</span></li>
441
+ </ul>
442
+ </div>
443
+ </div>
430
444
  <div class="archive-row" id="v1-38-0">
431
445
  <div class="archive-head"><a class="release-version" href="#v1-38-0">v1.38.0</a> <span class="release-date">23/10/2026</span></div>
432
446
  <div class="archive-body">
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.46.0" title="v1.46.0 docs · VI"><span class="dot"></span>v1.46.0</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.46.1" title="v1.46.1 docs · VI"><span class="dot"></span>v1.46.1</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new">Changelog</a>
@@ -37,7 +37,7 @@
37
37
  <span class="brand-mark"><img src="/assets/piagent-logo.svg" alt="" width="28" height="28" /></span>
38
38
  <span class="brand-name"><b>Piagent</b> <span>Platform</span></span>
39
39
  </a>
40
- <a class="version-badge" href="/whats-new" data-docs-version="v1.46.0" title="v1.46.0 docs · VI"><span class="dot"></span>v1.46.0</a>
40
+ <a class="version-badge" href="/whats-new" data-docs-version="v1.46.1" title="v1.46.1 docs · VI"><span class="dot"></span>v1.46.1</a>
41
41
  <nav class="top-nav" aria-label="Tài liệu">
42
42
  <a href="/quickstart">Tài liệu</a>
43
43
  <a href="/whats-new">Changelog</a>
@@ -256,8 +256,8 @@
256
256
  },
257
257
  {
258
258
  "path": "tests",
259
- "fileCount": 635,
260
- "sha256": "e192ed7528a5d5a992cd33c3837ee6f779b404e45cedc06f79978e2bfa9272eb"
259
+ "fileCount": 636,
260
+ "sha256": "143847ccd4abfed97f9fc6bc5f7e5d654eb1cf83507ed473f5d2344327b14d8e"
261
261
  }
262
262
  ],
263
263
  "custodianComparisonRules": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piagent/platform",
3
- "version": "1.46.0",
3
+ "version": "1.46.1",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -0,0 +1,61 @@
1
+ import { memberMessages, memberRequest } from './objections.mjs';
2
+
3
+ // Several helpers of one role, at the member's request only ("use 3
4
+ // reviewers", "cho 3 scout"): one delegate call runs 2 to MAX_COPIES helper
5
+ // sessions of the role side by side, each with its own context, and their
6
+ // answers merge into one. The main agent never adds helpers on its own, and
7
+ // the harness never does: the call quotes the member's words that ask for
8
+ // them, and those words must be the member's own and name the number. The
9
+ // copies share the role's grant, so Studio counts the role once while each
10
+ // copy counts against the role's calls for the message.
11
+ export const MAX_COPIES = 3;
12
+ const COUNTS = { two: 2, three: 3, hai: 2, ba: 3 };
13
+ // Words that say the number is of helpers, not of files or steps.
14
+ const HELPER_WORDS = /agent|helper|review|scout|research|verif|nghiên cứu|kiểm|xác minh|trinh sát/;
15
+ const normal = text => String(text ?? '').toLowerCase().normalize('NFC').replace(/\s+/g, ' ').trim();
16
+ // The largest helper count the words name (a digit, or two/three in English or Vietnamese).
17
+ export function namedCount(words) {
18
+ const found = [...normal(words).matchAll(/(?<![\p{L}\d])(\d+|two|three|hai|ba)(?![\p{L}\d])/gu)].map(m => COUNTS[m[1]] ?? Number(m[1]));
19
+ return found.length ? Math.max(...found) : 0;
20
+ }
21
+ // The copies a delegate call asks for, checked against the member's words;
22
+ // null for a plain call. `split`: one line per copy naming its share of the work.
23
+ export function fanOut(manager, role, args, harness) {
24
+ const split = Array.isArray(args?.split) ? args.split.map(s => typeof s === 'string' ? s.trim().slice(0, 500) : '').filter(Boolean) : [];
25
+ const n = split.length || (Number.isInteger(args?.copies) ? args.copies : 1);
26
+ if (n <= 1 && !args?.member_words) return null;
27
+ if (harness) throw Error('managed-copies-invalid: the harness runs one helper per call.');
28
+ if (n < 2 || n > MAX_COPIES) throw Error(`managed-copies-invalid: ask for 2 to ${MAX_COPIES} ${role} helpers (copies, or one split line per helper).`);
29
+ const words = normal(args?.member_words);
30
+ if (words.length < 3 || !memberRequest(memberMessages(manager)).some(text => normal(text).includes(words)) || namedCount(words) < n || !HELPER_WORDS.test(words))
31
+ throw Error(`managed-copies-not-asked: more than one ${role} helper runs only when the member asks for it. Quote in member_words the member's own words that ask for ${n} (verbatim, with the number); otherwise use one helper.`);
32
+ return { n, split: split.length ? split : null };
33
+ }
34
+ // What one copy reads under its brief.
35
+ export function copyNote(role, i, fan) {
36
+ return `\n\nThe member asked for ${fan.n} ${role} helpers; you are ${role} helper ${i + 1} of ${fan.n}. ${fan.split ? `Your share of the work: ${fan.split[i]}. The others cover the rest.` : 'The others have the same brief: answer on your own judgement.'}`;
37
+ }
38
+
39
+ const jsonOf = reply => {
40
+ for (const raw of [...String(reply).matchAll(/```json\s*([\s\S]*?)```/g)].map(m => m[1]).reverse()) { try { const v = JSON.parse(raw); if (v && typeof v === 'object') return v; } catch { /* next */ } }
41
+ return null;
42
+ };
43
+ const unique = (list, key) => { const seen = new Set(); return list.filter(x => { const k = key(x); if (seen.has(k)) return false; seen.add(k); return true; }); };
44
+ const SEVERITY = { blocking: 0, major: 1, minor: 2 };
45
+ // One answer from the copies: each helper's prose (its JSON left out), then
46
+ // one JSON block joining their findings, verdicts and objections, so the
47
+ // review, verify and objection readers see every copy. `parsed`: every copy
48
+ // gave its JSON. A finding or a failed claim any copy reports stands.
49
+ export function mergeCopies(role, label, answers, { findingsOf, issuesOf, verdictsOf }) {
50
+ const n = answers.length, room = Math.floor(12000 / n);
51
+ const findings = role === 'review' ? unique(answers.flatMap(a => findingsOf(a.reply) ?? []), f => `${f.severity}|${f.file}|${f.line}|${f.issue}`).sort((a, b) => SEVERITY[a.severity] - SEVERITY[b.severity]) : null;
52
+ const verdicts = role === 'verify' ? unique(answers.flatMap(a => verdictsOf(a.reply) ?? []), v => `${v.status}|${v.claim}`) : null;
53
+ const issues = unique(answers.flatMap(a => issuesOf(a.reply) ?? []), i => `${i.kind}|${i.detail}`).slice(0, 5);
54
+ const parsed = answers.every(a => jsonOf(a.reply));
55
+ const summaries = answers.map(a => jsonOf(a.reply)?.summary).filter(s => typeof s === 'string').map(s => s.slice(0, 200));
56
+ const json = { ...(findings ? { findings: findings.slice(0, 40) } : {}), ...(verdicts ? { verdicts: verdicts.slice(0, 40) } : {}), brief_issues: issues,
57
+ summary: `${n} ${role} helpers${summaries.length ? ': ' + summaries.join(' | ') : ''}`.slice(0, 900) };
58
+ const body = answers.map((a, i) => `### ${label} helper ${i + 1} of ${n}\n${String(a.reply).replace(/```json\s*[\s\S]*?```/g, '').trim().slice(0, room)}`).join('\n\n');
59
+ return { reply: `${body}\n\n\`\`\`json\n${JSON.stringify(json)}\n\`\`\``, usage: answers.reduce((total, a) => total + a.usage, 0),
60
+ ...(findings ? { findings, parsed, blocking: findings.filter(f => f.severity === 'blocking').length } : {}) };
61
+ }
@@ -7,7 +7,7 @@ export function shareGrant(owner, role) {
7
7
  owner.sharedGrants ??= new Map();
8
8
  const shared = owner.sharedGrants.get(role);
9
9
  if (shared && Date.now() - shared.at < 60_000) { shared.users++; return shared; }
10
- const entry = { at: Date.now(), users: 1, grant: owner.broker.request('renew', { role }) };
10
+ const entry = { at: Date.now(), users: 1, grant: owner.broker.request('renew', slotArgs(role)) };
11
11
  entry.grant.catch(() => { if (owner.sharedGrants.get(role) === entry) owner.sharedGrants.delete(role); });
12
12
  owner.sharedGrants.set(role, entry);
13
13
  return entry;
@@ -16,3 +16,11 @@ export function shareGrant(owner, role) {
16
16
  export function releaseGrant(owner, role, entry) {
17
17
  if (--entry.users <= 0 && owner.sharedGrants?.get(role) === entry) owner.sharedGrants.delete(role);
18
18
  }
19
+
20
+ // A helper the member asked for beside another of its role holds a grant of
21
+ // its own: its key is role#slot (slot 2 or 3), the broker's {role, slot}.
22
+ export const baseRole = key => String(key).split('#')[0];
23
+ export function slotArgs(key) {
24
+ const [role, slot] = String(key).split('#');
25
+ return slot ? { role, slot: Number(slot) } : { role };
26
+ }
@@ -44,7 +44,9 @@ export function helperPrompt(roles) {
44
44
  : `Use the research helper (delegate role "research") for anything on the web (current information, news, documentation) and ${WIDE_READING}; work from its findings and do small code lookups yourself.`);
45
45
  if (roles.includes('verify')) lines.push('Before you report a non-trivial result as done, you may have the verify helper (delegate role "verify") confirm it independently: it runs the repository checks on the current code and checks claims against the code and primary sources, answering pass, fail or unverifiable with evidence.');
46
46
  // Helpers read the member's request beside the brief and may object to it.
47
- if (lines.length) lines.push('A helper sees the member\'s request next to your brief and may object to the brief with evidence (brief_issues). You decide, but answer every objection: send a corrected brief, change your approach, or tell the member why it does not hold. A helper that objects twice for one message goes to the member.');
47
+ if (lines.length) lines.push('A helper sees the member\'s request next to your brief and may object to the brief with evidence (brief_issues); each objection gets an id. You decide, but check each one at the place its evidence names and record your decision with resolve_objection: accepted with what you changed (a corrected brief, the plan, the code), or rejected with what you checked and why. An objection that the brief rests on a wrong premise or conflicts with the request refuses file changes until you decide it; a rejected one goes back to the helper, and a disagreement that remains goes to the member, as does a second objection from the same helper for one message.');
48
+ // More than one helper of a role, only at the member's word (copies.mjs).
49
+ if (lines.length) lines.push('One helper per role works on a brief. Only when the member asks for several of one role ("use 3 subagents", "dùng 2 helper"), run up to 3 in one delegate call: copies for independent answers on the same brief, or split for shares of the work, with member_words quoting the member\'s request. Never add helpers on your own.');
48
50
  return lines.length ? ' ' + lines.join(' ') : '';
49
51
  }
50
52
  const HELPER_USES = {
@@ -52,10 +54,10 @@ const HELPER_USES = {
52
54
  research: roles => roles.includes('scout') ? 'research: anything on the web (current information, news, library or API documentation, releases, error messages), with the code when needed. It searches and reads the pages; you get its summary with sources.'
53
55
  : 'research: anything on the web (current information, news, documentation; it searches and reads the pages, you get its summary with sources), and before changing an area you do not know that spans more files than you can read in a few batches (where something is computed and every caller, how a flow crosses modules); your own context stays free for the change.',
54
56
  verify: () => 'verify: an independent check of a result: it runs the repository checks on the current code (offline, and the project is read-only for its commands) and confirms claims, yours or a helper\'s, against the code and primary sources. It answers pass, fail or unverifiable per claim, with evidence.',
55
- review: () => 'review: an independent review of the current patch (this turn\'s changes, commits made in the turn included). To review work committed in earlier turns, pass since: the commit, branch or HEAD~N it started from; when this turn changed nothing, it reviews the branch\'s commits since it left the default branch.',
57
+ review: () => 'review: an independent review of the current patch (this turn\'s changes, commits made in the turn included). To review work committed in earlier turns, pass since: the commit, branch or HEAD~N it started from; when this turn changed nothing, it reviews the branch\'s commits since it left the default branch. Ask once, when your change is finished, and again only after fixing blocking findings; under a required review the harness reviews the final patch and refuses your own call.',
56
58
  };
57
59
  export function delegateDescription(roles) {
58
- return `Hand work to a helper with its own context. Helpers read the project but cannot change files or call helpers. ${roles.map(role => HELPER_USES[role](roles)).join(' ')} A lookup that one grep or read answers stays with you. Write the task as a brief the helper can act on alone: the goal, what to find or check, where to start and what you already know or ruled out, and the answer you need (file:line facts). One call per helper runs at a time; each can be called again, up to ${HELPER_CALLS} times per user message, for example a second review after fixing what the first one found.`;
60
+ return `Hand work to a helper with its own context. Helpers read the project but cannot change files or call helpers. ${roles.map(role => HELPER_USES[role](roles)).join(' ')} A lookup that one grep or read answers stays with you. Write the task as a brief the helper can act on alone: the goal, what to find or check, where to start and what you already know or ruled out, and the answer you need (file:line facts). One call per helper runs at a time; each can be called again, up to ${HELPER_CALLS} times per user message, for example a second scout question on what the first answer opened.`;
59
61
  }
60
62
  // Scout and research end with their objections to the brief, if any.
61
63
  const OBJECTIONS = `${BRIEF_CHECK} End your answer with one JSON object in a \`\`\`json block: {${BRIEF_ISSUES_JSON}}, with an empty list when the brief is sound. You cannot change files or delegate.`;
@@ -7,12 +7,14 @@ import { fenced, HELPER_UNTRUSTED_RULE } from './untrusted.mjs';
7
7
  import { jobTitle } from './call-purpose.mjs';
8
8
  import { ManagedToolBoundary } from './tool-boundary.mjs';
9
9
  import { managedResourceLoader } from './resource-loader.mjs';
10
- import { shareGrant, releaseGrant } from './grant-share.mjs';
10
+ import { shareGrant, releaseGrant, slotArgs, baseRole } from './grant-share.mjs';
11
11
  import { describeFailure, failureCode, failureText } from '../runtime/managed-failure.mjs';
12
- import { reviewParts, mergeReviews, REVIEW_PARALLEL, emptySnapshot, readBranchBase, readPatchDigest, readPatchSnapshot, resolveReviewBase } from './patch-snapshot.mjs';
13
- import { reviewFindings, briefIssues } from './workflow.mjs';
12
+ import { reviewParts, focusParts, mergeReviews, REVIEW_PARALLEL, emptySnapshot, readBranchBase, readPatchDigest, readPatchSnapshot, resolveReviewBase } from './patch-snapshot.mjs';
13
+ import { reviewFindings, briefIssues, verifyVerdicts } from './workflow.mjs';
14
14
  import { currentWorkflow, helperWorkflowPrompt } from './workflow-context.mjs';
15
15
  import { anchorText, relayObjection, recordVerdicts } from './objections.mjs';
16
+ import { recordFindings } from './ledger.mjs';
17
+ import { copyNote, mergeCopies } from './copies.mjs';
16
18
  import { HELPER_CALLS, HELPER_SETUP, READ_TOOLS, countedCheck } from './helper-roles.mjs';
17
19
  import { trackToolFailures } from './tool-failures.mjs';
18
20
  import { helperMemoryTools, renewer } from './memory.mjs';
@@ -43,14 +45,16 @@ async function reviewSnapshot(managed, harness, since) {
43
45
  return branch ? Object.assign(await readPatchSnapshot(managed.boundary, null, branch.base), { from: { kind: 'branch', ref: branch.ref, base: branch.base } }) : snapshot;
44
46
  }
45
47
 
46
- export async function runHelper(managed, role, task, signal, harness, { verifyGrant, patiently, title, since }) {
47
- const snapshot = role === 'review' ? await reviewSnapshot(managed, harness, since) : null, parts = snapshot ? reviewParts(snapshot) : null;
48
+ export async function runHelper(managed, role, task, signal, harness, { verifyGrant, patiently, title, since, fan = null }) {
49
+ const snapshot = role === 'review' ? await reviewSnapshot(managed, harness, since) : null;
50
+ // The harness's re-review after a fix reads only the parts holding a file changed since (reviewFocus).
51
+ const parts = snapshot ? focusParts(reviewParts(snapshot), harness && !since ? managed.reviewFocus : null) : null;
48
52
  // A helper whose company model account is out of usage, or resting after
49
53
  // a refusal, starts once more: Studio then gives the new helper the next
50
54
  // model of its role that serves (an auto role's list). Helpers read and
51
55
  // run checks only, so starting over repeats no change.
52
56
  for (let attempt = 1; ; attempt += 1) {
53
- try { return await helperAttempt(managed, role, task, signal, harness, snapshot, parts, { verifyGrant, patiently, title }); }
57
+ try { return await helperAttempt(managed, role, task, signal, harness, snapshot, parts, { verifyGrant, patiently, title, fan }); }
54
58
  catch (error) {
55
59
  if (attempt >= HELPER_ATTEMPTS || signal?.aborted || !HELPER_SWITCH.has(failureCode(error?.message)) || (managed.helperCalls.get(role) ?? 0) >= HELPER_CALLS) throw error;
56
60
  }
@@ -59,13 +63,62 @@ export async function runHelper(managed, role, task, signal, harness, { verifyGr
59
63
  // A helper's role closes through the same wait as the run's close: a passing
60
64
  // failure of Agent Watch's way to Studio is asked again. A close that still
61
65
  // fails leaves the helper's answer standing: the role closes with the run,
62
- // and the next helper of the role closes it first.
63
- export async function closeRole(managed, role) {
64
- try { await viaLink(managed.studioLink, () => managed.broker.request('close', { role })); managed.unclosed?.delete(role); }
65
- catch { (managed.unclosed ??= new Set()).add(role); }
66
+ // and the next helper of the role closes it first. `key`: the role, or
67
+ // role#slot for a helper the member asked for beside it (grant-share.mjs).
68
+ export async function closeRole(managed, key) {
69
+ try { await viaLink(managed.studioLink, () => managed.broker.request('close', slotArgs(key))); managed.unclosed?.delete(key); }
70
+ catch { (managed.unclosed ??= new Set()).add(key); }
66
71
  }
67
- async function helperAttempt(managed, role, task, signal, harness, snapshot, parts, { verifyGrant, patiently, title }) {
68
- if (managed.unclosed?.has(role)) await closeRole(managed, role);
72
+ // Helpers the member asked for run on grants of their own (slots) when
73
+ // Studio and Agent Watch hold several of a role at once; otherwise side by
74
+ // side on the role's one grant, where Studio takes their requests in turn.
75
+ const slotsReady = managed => (managed.manifest?.broker_features ?? []).includes('helper-slots');
76
+
77
+ async function helperAttempt(managed, role, task, signal, harness, snapshot, parts, { verifyGrant, patiently, title, fan }) {
78
+ const setup = HELPER_SETUP[role], opts = { verifyGrant, patiently, title };
79
+ let result;
80
+ if (!fan) result = await answerUnder(managed, role, task, signal, harness, snapshot, parts, opts, ['']);
81
+ else if (!slotsReady(managed)) result = await answerUnder(managed, role, task, signal, harness, snapshot, parts, opts, Array.from({ length: fan.n }, (_, i) => copyNote(role, i, fan)));
82
+ else {
83
+ // One grant per helper, all at once; the first that fails stops the others.
84
+ const stop = new AbortController(), both = AbortSignal.any([stop.signal, ...(signal ? [signal] : [])]);
85
+ let results;
86
+ try { results = await Promise.all(Array.from({ length: fan.n }, (_, i) => answerUnder(managed, i ? `${role}#${i + 1}` : role, task, both, harness, snapshot, parts, opts, [copyNote(role, i, fan)]).catch(error => { stop.abort(); throw error; }))); }
87
+ catch (error) { throw signal?.aborted ? Error('managed-helper-cancelled') : error; }
88
+ const merged = mergeCopies(role, setup.label, results, { findingsOf: reviewFindings, issuesOf: briefIssues, verdictsOf: verifyVerdicts });
89
+ result = { ...results[0], reply: merged.reply, usage: merged.usage, review: merged.findings ? merged : null, stale: results.some(r => r.stale), report: null };
90
+ }
91
+ const { reply, usage, review, stale, from, grant } = result;
92
+ const details = { role, runID: grant.run_id, tokens: usage, patchDigest: snapshot?.digest, stale, ...(fan ? { helpers: fan.n, ...(slotsReady(managed) ? { slots: true } : {}) } : {}), ...(from ? { since: { kind: from.kind, ref: from.ref, base: from.base } } : {}) };
93
+ if (snapshot) {
94
+ // Severity-graded findings decide whether the harness sends the agent back.
95
+ const findings = review ? review.findings : reviewFindings(reply);
96
+ Object.assign(details, { parsed: review ? review.parsed : !!findings, findings: (findings ?? []).slice(0, 40), blocking: findings?.filter(f => f.severity === 'blocking').length ?? 0 });
97
+ if (review) Object.assign(details, { parts: parts.parts.length, unread: parts.omitted.map(o => o.path).slice(0, 50) });
98
+ if (managed.run) { managed.run.reviews += 1; managed.run.blocking += details.blocking; }
99
+ // A review of earlier commits is not the review of this turn's change;
100
+ // what each file held then tells a later re-review what changed since.
101
+ if (!from) { managed.review = details; if (!stale) { recordFindings(managed, findings); managed.reviewFiles = await managed.changedFiles?.(); } }
102
+ managed.session.sessionManager.appendCustomEntry('agent-watch-review', details);
103
+ }
104
+ const verdicts = role === 'verify' ? recordVerdicts(managed, reply, harness, details) : '';
105
+ const report = result.report ?? answeredSettlement(role, reply, details, managed.helperSpecs?.get(role) ?? {}); details.outcome = report.outcome;
106
+ await managed.session.sendCustomMessage({ customType: 'agent-watch-helper-receipt', display: true,
107
+ content: `${setup.label}: ${usage.toLocaleString('en-US')} tokens${verdicts}${fan ? ` · ${fan.n} helpers` : review ? ` · ${parts.parts.length} parts` : ''}${snapshot ? (stale ? ' · review is stale' : ' · patch ' + snapshot.digest.slice(0, 12)) : ''} · ${OUTCOME_WORDS[report.outcome]}.`, details }, { triggerTurn: false });
108
+ // An objection to the brief comes with the answer; a stale review has none to act on.
109
+ const objection = stale ? '' : await relayObjection(managed, role, reply, harness);
110
+ const ending = `Outcome: ${report.outcome}${report.outcome === 'unverifiable' ? ` (${report.summary})` : ''}.\n\n`;
111
+ // The helper read untrusted content: its answer is data for the main agent.
112
+ return { content: [{ type: 'text', text: ending + (stale ? 'Review is stale: the patch changed during review. Ask for another review only after fixing blocking findings; under a required review the harness reviews the final patch.' : fenced(`${setup.label} helper's answer`, reply) + objection) }], details };
113
+ }
114
+
115
+ // One grant of the role (`key`: the role, or role#slot) and the helper
116
+ // sessions that answer under it, one per note (a copy's place among the
117
+ // helpers the member asked for). Its settlement goes to Studio and the grant
118
+ // closes before the answer is used.
119
+ async function answerUnder(managed, key, task, signal, harness, snapshot, parts, { verifyGrant, patiently, title }, notes) {
120
+ const role = baseRole(key);
121
+ if (managed.unclosed?.has(key)) await closeRole(managed, key);
69
122
  // A long local tool step may outlive the main lease. Refresh authority
70
123
  // before asking for a child; the broker serializes concurrent renewals,
71
124
  // and main's own searches running beside it share managed renew.
@@ -75,18 +128,18 @@ async function helperAttempt(managed, role, task, signal, harness, snapshot, par
75
128
  try {
76
129
  const parent = await share.grant;
77
130
  verifyGrant(parent, managed.manifest, 'main'); managed.grant = parent;
78
- grant = await patiently(() => managed.broker.request('child', { role }), 2);
131
+ grant = await patiently(() => managed.broker.request('child', slotArgs(key)), 2);
79
132
  } finally { releaseGrant(managed, 'main', share); }
80
- managed.helperCalls.set(role, (managed.helperCalls.get(role) ?? 0) + 1);
81
- try { verifyGrant(grant, managed.manifest, role); }
82
- catch (error) { try { await managed.broker.request('close', { role }); } catch { /* closes with the run */ } throw error; }
133
+ managed.helperCalls.set(role, (managed.helperCalls.get(role) ?? 0) + notes.length); // each helper the member asked for counts
134
+ try { verifyGrant(grant, managed.manifest, key); }
135
+ catch (error) { try { await managed.broker.request('close', slotArgs(key)); } catch { /* closes with the run */ } throw error; }
83
136
  } catch (error) { throw Error(`managed-helper-failed: ${describeFailure(role, failureText(error))}`); }
84
137
  let boundary, report = null;
85
- const sessions = new Set(), spec = managed.helperSpecs?.get(role) ?? {};
86
- const renew = renewer(managed, role, g => verifyGrant(g, managed.manifest, role));
138
+ const sessions = new Set(), watched = new Set(), spec = managed.helperSpecs?.get(role) ?? {};
139
+ const renew = renewer(managed, key, g => verifyGrant(g, managed.manifest, key));
87
140
  try {
88
141
  const runtime = await managed.api.ModelRuntime.create({ credentials: new managed.ai.InMemoryCredentialStore(), modelsPath: null, refreshOnCreate: false, allowModelNetwork: false });
89
- const model = managed.installModel(runtime, grant); managed.wrapStreams(runtime, role);
142
+ const model = managed.installModel(runtime, grant); managed.wrapStreams(runtime, key);
90
143
  // Studio's logs name the job by its title (the brief's first line without one).
91
144
  (managed.jobTitles ??= new WeakMap()).set(runtime, jobTitle(task, title));
92
145
  const setup = HELPER_SETUP[role], memory = helperMemoryTools(managed, () => grant, renew);
@@ -95,15 +148,15 @@ async function helperAttempt(managed, role, task, signal, harness, snapshot, par
95
148
  // One helper session answers the brief (with one part of the patch for a
96
149
  // review in parts); its reply and tokens. A helper runs at its role's
97
150
  // level from Studio (fixed by the Harness, or its main agent's).
98
- const ask = async extra => {
151
+ const ask = async (extra, note = '', who = key) => {
99
152
  const { session } = await managed.api.createAgentSession({ cwd: managed.cwd, model, modelRuntime: runtime, thinkingLevel: grant.effort || 'off',
100
153
  settingsManager: managed.api.SettingsManager.inMemory({ retry: { enabled: false, provider: { maxRetries: 0 } }, cacheWarming: 'off' }),
101
154
  sessionManager: managed.api.SessionManager.inMemory(managed.cwd), noTools: 'builtin', tools: [...READ_TOOLS, ...(setup.commands ? ['bash'] : []), ...(setup.web ? ['web_search', 'web_fetch'] : []), ...memory.map(tool => tool.name)],
102
155
  customTools: [...boundary.tools(managed.api).filter(tool => READ_TOOLS.includes(tool.name) || setup.commands && tool.name === 'bash').map(tool => countedCheck(managed, tool)),
103
- ...(setup.web ? managed.webTools(runtime, role) : []), ...memory], resourceLoader: managedResourceLoader(managed.api, { systemPrompt: setup.prompt + HELPER_UNTRUSTED_RULE }) });
104
- sessions.add(session); trackToolFailures(managed, session, role);
156
+ ...(setup.web ? managed.webTools(runtime, key) : []), ...memory], resourceLoader: managedResourceLoader(managed.api, { systemPrompt: setup.prompt + HELPER_UNTRUSTED_RULE }) });
157
+ sessions.add(session); trackToolFailures(managed, session, role); watched.add(who); watchHelper(managed, who, session, task, title);
105
158
  const abort = () => void session.abort(); signal?.addEventListener('abort', abort, { once: true });
106
- try { if (signal?.aborted) throw Error('managed-helper-cancelled'); await session.prompt(anchorText(managed.session.sessionManager, harness) + task + helperWorkflowPrompt(currentWorkflow(managed.session.sessionManager)) + checks + extra, { expandPromptTemplates: false }); }
159
+ try { if (signal?.aborted) throw Error('managed-helper-cancelled'); await session.prompt(anchorText(managed.session.sessionManager, harness) + task + note + helperWorkflowPrompt(currentWorkflow(managed.session.sessionManager)) + checks + extra, { expandPromptTemplates: false }); }
107
160
  finally { signal?.removeEventListener('abort', abort); }
108
161
  const messages = session.messages.filter(m => m.role === 'assistant');
109
162
  // The main agent (and the member) learn why a helper failed, not just that it did.
@@ -112,50 +165,65 @@ async function helperAttempt(managed, role, task, signal, harness, snapshot, par
112
165
  if (!messages.length || messages.at(-1).stopReason === 'aborted') throw Error(signal?.aborted ? 'managed-helper-cancelled' : 'managed-helper-failed');
113
166
  return { reply: messages.map(textContent).join('\n'), usage: messages.reduce((n, m) => n + (m.usage?.totalTokens ?? 0), 0) };
114
167
  };
115
- let reply, usage, review = null;
116
- if (!parts || parts.parts.length === 1) {
117
- ({ reply, usage } = await ask(parts ? `\n${parts.parts[0].text}` : ''));
118
- reply = reply.slice(0, 16000);
119
- } else {
168
+ // One helper's answer: the brief (with the whole patch, or the patch in parts for a large review).
169
+ const once = async (note = '', who = key) => {
170
+ if (!parts || parts.parts.length === 1) { const answer = await ask(parts ? `\n${parts.parts[0].text}` : '', note, who); return { ...answer, reply: answer.reply.slice(0, 16000), review: null }; }
120
171
  // A large patch: one reviewer per part, REVIEW_PARALLEL at a time (Studio
121
172
  // admits one request of a role at a time); the first part that fails
122
173
  // stops the others.
123
174
  const answers = new Array(parts.parts.length);
124
175
  let next = 0;
125
176
  const reviewer = async () => {
126
- while (next < parts.parts.length) { const i = next++; answers[i] = await ask(`\n${parts.parts[i].text}`); }
177
+ while (next < parts.parts.length) { const i = next++; answers[i] = await ask(`\n${parts.parts[i].text}`, note, who); }
127
178
  };
128
179
  try { await Promise.all(Array.from({ length: Math.min(REVIEW_PARALLEL, parts.parts.length) }, reviewer)); }
129
180
  catch (error) { next = parts.parts.length; for (const session of sessions) void session.abort(); throw error; }
130
- review = mergeReviews(parts, answers, { findingsOf: reviewFindings, issuesOf: briefIssues });
131
- ({ reply, usage } = review);
181
+ const review = mergeReviews(parts, answers, { findingsOf: reviewFindings, issuesOf: briefIssues });
182
+ return { reply: review.reply, usage: review.usage, review };
183
+ };
184
+ let reply, usage, review = null;
185
+ if (notes.length === 1) ({ reply, usage, review } = await once(notes[0]));
186
+ else {
187
+ // Helpers the member asked for on this one grant, each on its own; the first that fails stops the others.
188
+ let answers;
189
+ try { answers = await Promise.all(notes.map((note, i) => once(note, i ? `${role}#${i + 1}` : key))); }
190
+ catch (error) { for (const session of sessions) void session.abort(); throw error; }
191
+ const merged = mergeCopies(role, setup.label, answers, { findingsOf: reviewFindings, issuesOf: briefIssues, verdictsOf: verifyVerdicts });
192
+ ({ reply, usage } = merged); review = merged.findings ? merged : null;
132
193
  }
133
194
  const from = snapshot?.from ?? null;
134
195
  const stale = snapshot ? (await (from ? readPatchDigest(managed.boundary, null, from.base) : managed.patchDigest())) !== snapshot.digest : false;
135
- const details = { role, runID: grant.run_id, tokens: usage, patchDigest: snapshot?.digest, stale, ...(from ? { since: { kind: from.kind, ref: from.ref, base: from.base } } : {}) };
136
- if (snapshot) {
137
- // Severity-graded findings decide whether the harness sends the agent back.
138
- const findings = review ? review.findings : reviewFindings(reply);
139
- Object.assign(details, { parsed: review ? review.parsed : !!findings, findings: (findings ?? []).slice(0, 40), blocking: findings?.filter(f => f.severity === 'blocking').length ?? 0 });
140
- if (review) Object.assign(details, { parts: parts.parts.length, unread: parts.omitted.map(o => o.path).slice(0, 50) });
141
- if (managed.run) { managed.run.reviews += 1; managed.run.blocking += details.blocking; }
142
- // A review of earlier commits is not the review of this turn's change.
143
- if (!from) managed.review = details;
144
- managed.session.sessionManager.appendCustomEntry('agent-watch-review', details);
145
- }
146
- const verdicts = role === 'verify' ? recordVerdicts(managed.run, reply, harness, details) : '';
147
- report = answeredSettlement(role, reply, details, spec); details.outcome = report.outcome;
148
- await managed.session.sendCustomMessage({ customType: 'agent-watch-helper-receipt', display: true,
149
- content: `${setup.label}: ${usage.toLocaleString('en-US')} tokens${verdicts}${review ? ` · ${parts.parts.length} parts` : ''}${snapshot ? (stale ? ' · review is stale' : ' · patch ' + snapshot.digest.slice(0, 12)) : ''} · ${OUTCOME_WORDS[report.outcome]}.`, details }, { triggerTurn: false });
150
- // An objection to the brief comes with the answer; a stale review has none to act on.
151
- const objection = stale ? '' : await relayObjection(managed, role, reply, harness);
152
- const ending = `Outcome: ${report.outcome}${report.outcome === 'unverifiable' ? ` (${report.summary})` : ''}.\n\n`;
153
- // The helper read untrusted content: its answer is data for the main agent.
154
- return { content: [{ type: 'text', text: ending + (stale ? 'Review is stale: the patch changed during review. Obtain a new review for the current patch.' : fenced(`${setup.label} helper's answer`, reply) + objection) }], details };
196
+ const findings = snapshot ? (review ? review.findings : reviewFindings(reply)) : null;
197
+ report = answeredSettlement(role, reply, snapshot ? { stale, parsed: review ? review.parsed : !!findings, findings: findings ?? [] } : {}, spec);
198
+ return { reply, usage, review, stale, from, grant, report };
155
199
  } catch (error) { report = failedSettlement(role, error, signal, spec); throw error; }
156
200
  finally {
157
201
  for (const session of sessions) session.dispose(); await boundary?.dispose();
158
202
  await settleHelper(managed, renew.lease, report); // with the role's current token, before it closes
159
- await closeRole(managed, role);
203
+ await closeRole(managed, key);
204
+ for (const who of watched) helperState(managed, who, null);
160
205
  }
161
206
  }
207
+
208
+ // The helpers running now and what each is doing, told to the session's
209
+ // listeners (the terminal's subagent panel, the WebUI) as it changes.
210
+ export function helperState(managed, key, state) {
211
+ const states = managed.helperStates ??= new Map();
212
+ if (state) states.set(key, state); else states.delete(key);
213
+ managed.notify?.({ type: 'managed_helper_state', helpers: [...states].map(([k, s]) => ({ key: k, role: baseRole(k), slot: slotArgs(k).slot ?? 1, ...s })) });
214
+ }
215
+ // What a helper is doing, for the terminal's subagent panel: the job and its
216
+ // latest tool step, told to listeners as it changes (company-terminal.mjs).
217
+ function watchHelper(managed, key, session, task, title) {
218
+ const job = String(title || task.split('\n').find(line => line.trim()) || '').trim().slice(0, 120);
219
+ helperState(managed, key, { job, step: 'đọc brief' });
220
+ session.subscribe?.(event => {
221
+ if (event?.type !== 'tool_execution_start') return;
222
+ helperState(managed, key, { job, step: stepText(event.toolName, event.args) });
223
+ });
224
+ }
225
+ 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
+ export function stepText(tool, args = {}) {
227
+ const what = args?.path ?? args?.pattern ?? args?.command ?? args?.query ?? args?.url ?? '';
228
+ return `${STEP[tool] ?? tool}${what ? ' ' + String(what).replace(/\s+/g, ' ').slice(0, 80) : ''}`;
229
+ }
@@ -24,7 +24,11 @@ export function delegateParameters() {
24
24
  title: { type: 'string', maxLength: 120, description: 'One line naming the job, for the company logs (for example "Find where login tokens are stored").' },
25
25
  task: { type: 'string', minLength: 1, maxLength: 12000 },
26
26
  ...Object.fromEntries(Object.entries(SPEC_FIELDS).map(([name, description]) => [name, field(description)])),
27
- since: { type: 'string', maxLength: 200, description: 'Review only: the commit, branch or HEAD~N the work to review started from (work committed in earlier turns).' } },
27
+ since: { type: 'string', maxLength: 200, description: 'Review only: the commit, branch or HEAD~N the work to review started from (work committed in earlier turns).' },
28
+ // Several helpers of the role, only at the member's request (copies.mjs).
29
+ copies: { type: 'integer', minimum: 2, maximum: 3, description: 'Only when the member asked for several helpers of this role: how many (2 or 3) run side by side on this brief. Never on your own.' },
30
+ split: { type: 'array', minItems: 2, maxItems: 3, items: { type: 'string', minLength: 1, maxLength: 500 }, description: 'Instead of copies: one line per helper naming its share of the work, when the member asked for several and the work divides.' },
31
+ member_words: { type: 'string', maxLength: 300, description: 'With copies or split: the member\'s own words, verbatim, that ask for that many helpers.' } },
28
32
  required: ['role', 'task'], additionalProperties: false };
29
33
  }
30
34
  export const SPEC_PROMPT = ' Give a helper target and acceptance at least (also change, constraints and ownership when they matter): it answers against them, and its ending is recorded with them.';