@tea-agent/loop-agent 0.44.0-next.13 → 0.44.0-next.14

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 (145) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +12 -12
  3. package/dist/adapters/loop-agent.js +34 -0
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +5 -5
  6. package/dist/cli/program.js +1 -4
  7. package/dist/commands/init-legacy-audit.js +330 -0
  8. package/dist/commands/init-upgrade.js +644 -104
  9. package/dist/commands/init.js +685 -330
  10. package/dist/commands/operator-guide.js +99 -0
  11. package/dist/commands/operator.js +16 -3
  12. package/dist/executors/pi-sdk-executor.js +3 -1
  13. package/dist/governance/harness.js +2 -7
  14. package/dist/governance/manifest-types.js +28 -9
  15. package/dist/governance/project-capability-scanner.js +263 -0
  16. package/dist/governance/project-config.js +96 -0
  17. package/dist/governance/resource-catalog.js +151 -0
  18. package/dist/infrastructure/console/app-data.js +6 -2
  19. package/dist/shared/operator/capabilities.js +2 -2
  20. package/dist/shared/package-metadata.js +23 -7
  21. package/dist/shared/pi-context-pressure/extension.js +23 -0
  22. package/dist/shared/pi-context-pressure/index.js +1 -0
  23. package/dist/shared/pi-context-pressure/telemetry.js +4 -0
  24. package/dist/shared/pi-context-pressure/tool-result-pruner.js +98 -0
  25. package/dist/shared/update/npm-client.js +49 -1
  26. package/dist/worker/cli.js +3 -5
  27. package/dist/worker/console/chat/browser-automation.js +114 -5
  28. package/dist/worker/console/chat/browser-routes.js +21 -0
  29. package/dist/worker/console/chat/chat-event-store.js +7 -6
  30. package/dist/worker/console/chat/context-insights.js +84 -19
  31. package/dist/worker/console/chat/pet/pet-affinity.js +169 -0
  32. package/dist/worker/console/chat/pet/pet-persist.js +109 -0
  33. package/dist/worker/console/chat/pet/pet-registry.js +142 -0
  34. package/dist/worker/console/chat/pet/pet-remarks.js +48 -0
  35. package/dist/worker/console/chat/pet/pet-routes.js +125 -0
  36. package/dist/worker/console/chat/pet/pet-service.js +262 -0
  37. package/dist/worker/console/chat/pet/pet-state.js +99 -0
  38. package/dist/worker/console/chat/pi-mode-loop-isolation.js +17 -2
  39. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +2 -2
  40. package/dist/worker/console/chat/pi-runtime.js +7 -2
  41. package/dist/worker/console/chat/routes.js +43 -7
  42. package/dist/worker/console/chat/scheduled-goal-request.js +14 -6
  43. package/dist/worker/console/chat/session-store.js +20 -3
  44. package/dist/worker/console/console-shutdown.js +34 -0
  45. package/dist/worker/console/server.js +2 -1
  46. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BA7qCL_r.js → abnfDiagram-N423BO3Z-DASc69AM.js} +1 -1
  47. package/dist/worker/console/static/assets/{arc-Df09ajwL.js → arc-h09p-LAc.js} +1 -1
  48. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CpF9sw5R.js → architectureDiagram-T3A2C74G-CZ3dtLkh.js} +1 -1
  49. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BtPys8ja.js → blockDiagram-VBNYF7ZC-DMrRhMXG.js} +1 -1
  50. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DZPnTWqT.js → c4Diagram-5PPSVZJV-DQxisHV6.js} +1 -1
  51. package/dist/worker/console/static/assets/channel-D1zgKZfg.js +1 -0
  52. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BD9D4h1p.js → chunk-2GRJ4B5K-CefVN9zh.js} +1 -1
  53. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Svn8yBmn.js → chunk-2Q5K7J3B-DmR1Koq2.js} +1 -1
  54. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-B8EbQj-S.js → chunk-5RXB4S5H-NlzrSW7L.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-tENZpcpF.js → chunk-5VM5RSS4-DfelJv-0.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DBx71Lco.js → chunk-6Q2QTUOP-BK6RFy58.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-1v_QamMz.js → chunk-GF5L2VYU-BAeh9lz4.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BAMCNNBG.js → chunk-JWPE2WC7-ChPS5WzG.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BvuEury-.js → chunk-KBJHAD2P-BoVe5IM6.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-YqFuHp8s.js → chunk-RYQCIY6F-C4q_0Lw9.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-ouI5T7Mr.js → chunk-XXDRQBXY-Bb9TvjeY.js} +1 -1
  62. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CXNNsY2M.js +1 -0
  63. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CXNNsY2M.js +1 -0
  64. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-Bri0_Izk.js → cose-bilkent-JH36ORCC-DmNnL8Ff.js} +1 -1
  65. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-e8Vhhi4X.js → cynefin-VYW2F7L2-Contvx58.js} +1 -1
  66. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-tSIXGQiJ.js → cynefinDiagram-MW4NZA55-Cigq7cjk.js} +1 -1
  67. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Rl-sF5pt.js → dagre-VZM6K2ZE-dEbC0nxb.js} +1 -1
  68. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Db6ddPz8.js → diagram-7IWD3JNH-CzIztESV.js} +1 -1
  69. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Jwd_Xw5S.js → diagram-B4RE2ZJO-Bo14zbaY.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CeC_8InC.js → diagram-LBJQPF4R-C4xUyil6.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LKPRaNq8.js → diagram-Q27KOJAE-CaBY_uYa.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DlVQytMW.js → diagram-UB23O5K3-ClUXUeiu.js} +1 -1
  73. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DnfJZwO-.js → ebnfDiagram-BXEA7PRR-BbGCgjC6.js} +1 -1
  74. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-3axVSXca.js → erDiagram-JOGREHBK-Dr-AcwSl.js} +1 -1
  75. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-xe04-_L-.js → flowDiagram-UKHOOZJN-BG8Gcfyc.js} +1 -1
  76. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-hpmRINiD.js → ganttDiagram-PKOTCBZU-CFVaPs5r.js} +1 -1
  77. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-6HSNz9Ko.js → gitGraphDiagram-DS77QQ5N-ChJ8pm-O.js} +1 -1
  78. package/dist/worker/console/static/assets/index-B_5GhVIO.js +488 -0
  79. package/dist/worker/console/static/assets/index-C7YMBluX.css +1 -0
  80. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BF6ksWTX.js → infoDiagram-6WML65LV-C5J9-uCZ.js} +1 -1
  81. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DXisdeD7.js → ishikawaDiagram-WSZJBQD7-BcfjqzDv.js} +1 -1
  82. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BWu65wmu.js → journeyDiagram-NVQOT4AX-BzzCO-rD.js} +1 -1
  83. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-hfNhHqAI.js → kanban-definition-27J2QSJJ-NCHUZ46h.js} +1 -1
  84. package/dist/worker/console/static/assets/{linear-D6HTfQcp.js → linear-BKZlWCUU.js} +1 -1
  85. package/dist/worker/console/static/assets/{mermaid.core-Iv91R1WP.js → mermaid.core-DbTaQjFE.js} +5 -5
  86. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CfPChwjR.js → mindmap-definition-FAOFIHXS-CroYl5xQ.js} +1 -1
  87. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B7TcoqHz.js → pegDiagram-VL7TDLO6-DgMBx-9k.js} +1 -1
  88. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-ouyPKMtQ.js → pieDiagram-7S7Q4E2Y-CyWDmbq1.js} +1 -1
  89. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B9tuyLbh.js → quadrantDiagram-CIZ2JOQS-BgGEOqSP.js} +1 -1
  90. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Ck4q9nJs.js → railroadDiagram-AXF67PYL-BMZ7oeeV.js} +1 -1
  91. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-wXLX5Dnp.js → requirementDiagram-LRYGKXZP-DxqyGhcc.js} +1 -1
  92. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D2XWw6_m.js → sankeyDiagram-W5VNT64P-DnSvNCG8.js} +1 -1
  93. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DzxKiug4.js → sequenceDiagram-SI44F4Z6-BiZh6f-R.js} +1 -1
  94. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWUqK5Us.js → sizeCapture-X5ZJPWSS-BKXSI8WB.js} +1 -1
  95. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D5Pa4O5g.js → stateDiagram-OKZ733FA-CfSgoYFm.js} +1 -1
  96. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DgHi3vjb.js +1 -0
  97. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-VH7cRnsJ.js → swimlanes-SLNWSIFB-CyGADcHe.js} +2 -2
  98. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bgvdt1Y0.js +8 -0
  99. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C4HM5NoZ.js → timeline-definition-Z64GVDOM-HmPaSPOK.js} +1 -1
  100. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Bu-4hOlA.js → vennDiagram-T6HMQDX7-VA2VBbez.js} +1 -1
  101. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CasFEvvl.js → wardleyDiagram-T6FBY63Y-BxjiBPZa.js} +1 -1
  102. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D-gpwPSA.js → xychartDiagram-ELKLHX3M--hUY-qFj.js} +1 -1
  103. package/dist/worker/console/static/index.html +2 -2
  104. package/dist/worker/console/static/pets/ember/pet.json +167 -0
  105. package/dist/worker/console/static/pets/ember/spritesheet.png +0 -0
  106. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +4 -3
  107. package/dist/worker/console/static-src/operator-chat/use-stat-dialog.js +95 -0
  108. package/dist/worker/console/workspace-context.js +14 -0
  109. package/dist/worker/console/workspace-initialization.js +25 -16
  110. package/dist/worker/delivery/package.js +4 -1
  111. package/dist/worker/observe/static/operator-chrome.js +1 -42
  112. package/dist/workflows/dag/backend-test-pytest-collection.js +4 -1
  113. package/dist/workflows/dag/final-verification.js +45 -10
  114. package/dist/workflows/dag/hybrid/sources.js +14 -14
  115. package/dist/workflows/dag/hybrid/templates/backend-test.js +5 -5
  116. package/dist/workflows/dag/hybrid/templates/frontend-test.js +4 -4
  117. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +6 -6
  118. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +5 -5
  119. package/dist/workflows/dag/skill-instructions.js +102 -32
  120. package/dist/workflows/dag/skill-snapshot.js +3 -0
  121. package/dist/workflows/dag/skills.js +7 -7
  122. package/dist/workflows/dag/validate.js +2 -2
  123. package/dist/workflows/dag/workspace-checkpoint.js +13 -1
  124. package/docs/architecture/runtime-boundaries.md +28 -22
  125. package/docs/init-surface.manifest.json +43 -295
  126. package/docs/templates/agent-dag.base.json +9 -9
  127. package/docs/templates/agent-dag.final-verification.json +6 -6
  128. package/docs/templates/agent-dag.supervised-implementation.json +12 -12
  129. package/docs/templates/frontend-implementation-dag.json +2 -2
  130. package/docs/templates/frontend-test-dag.json +2 -2
  131. package/docs/templates/hybrid-dag.json +9 -9
  132. package/docs/templates/knowledge-sync-dag.json +8 -8
  133. package/package.json +6 -2
  134. package/resource-catalog.v1.json +1973 -0
  135. package/resources/agent-bridge/loop-agent/SKILL.md +40 -0
  136. package/resources/init-history/legacy-surface.v1.json +33 -0
  137. package/skills/init-update/SKILL.md +34 -0
  138. package/skills/init-update/agents/openai.yaml +6 -0
  139. package/dist/worker/console/static/assets/channel-DiTNLZUm.js +0 -1
  140. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgL8aiyZ.js +0 -1
  141. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgL8aiyZ.js +0 -1
  142. package/dist/worker/console/static/assets/index-C_8_gWal.css +0 -1
  143. package/dist/worker/console/static/assets/index-DybcVXCu.js +0 -488
  144. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C0QIbqO4.js +0 -1
  145. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BB9CzRHB.js +0 -8
@@ -57,8 +57,18 @@ export class BrowserAutomationService {
57
57
  disposed = false;
58
58
  dialogQueue = [];
59
59
  downloadQueue = [];
60
+ mirrorPage;
61
+ mirrorSession;
62
+ mirrorSeq = 0;
63
+ mirrorFrame;
60
64
  constructor(options) {
61
65
  this.options = options;
66
+ // The visible window stays the default so human picking works out of the
67
+ // box. LOOP_BROWSER_HEADLESS=1 (or "true"/"on") switches the browser to a
68
+ // background (headless) instance that never opens a window.
69
+ if (this.options.headless === undefined) {
70
+ this.options.headless = browserHeadlessEnabled();
71
+ }
62
72
  }
63
73
  /** Runtime entrypoint: tools and HTTP must enter once, binding the target before queuing. */
64
74
  run(input, operation) {
@@ -156,13 +166,14 @@ export class BrowserAutomationService {
156
166
  */
157
167
  async state() {
158
168
  if (!this.browserPromise && !this.disposed)
159
- return emptyBrowserState();
169
+ return { ...emptyBrowserState(), headless: this.options.headless === true };
160
170
  // Polling never starts a context, changes selection, or waits on a quarantined page.
161
171
  const page = this.operations.current?.target.page ?? this.activePage;
162
172
  if (!page || page.isClosed())
163
- return { ...emptyBrowserState(), running: !!this.browserPromise, tabs: await this.tabSummaries() };
173
+ return { ...emptyBrowserState(), running: !!this.browserPromise, headless: this.options.headless === true, tabs: await this.tabSummaries() };
164
174
  const state = {
165
175
  running: true,
176
+ headless: this.options.headless === true,
166
177
  tabId: this.trackPage(page),
167
178
  url: page.url(),
168
179
  title: this.recovering.has(page) ? "Page busy — script may still be running" : await page.title().catch(() => ""),
@@ -172,6 +183,36 @@ export class BrowserAutomationService {
172
183
  };
173
184
  return state;
174
185
  }
186
+ /**
187
+ * Passive screencast frame of the active page for the panel mirror view.
188
+ * Like state(), polling never launches the browser and never touches the
189
+ * operation queue, so a stuck page cannot block the panel.
190
+ */
191
+ async panelFrame(since = 0) {
192
+ if (!this.browserPromise || this.disposed) {
193
+ await this.stopMirror().catch(() => undefined);
194
+ return { running: false, changed: false, seq: this.mirrorSeq };
195
+ }
196
+ const page = this.operations.current?.target.page ?? this.activePage;
197
+ if (!page || page.isClosed()) {
198
+ return { running: true, changed: false, seq: this.mirrorSeq };
199
+ }
200
+ if (page !== this.mirrorPage || !this.mirrorSession) {
201
+ await this.startMirror(page);
202
+ }
203
+ const frame = this.mirrorFrame;
204
+ if (!frame || this.mirrorSeq <= since) {
205
+ return { running: true, changed: false, seq: this.mirrorSeq };
206
+ }
207
+ return {
208
+ running: true,
209
+ changed: true,
210
+ seq: this.mirrorSeq,
211
+ url: page.url(),
212
+ title: await page.title().catch(() => ""),
213
+ image: `data:image/jpeg;base64,${frame.data}`,
214
+ };
215
+ }
175
216
  async search(rawQuery, rawUrl) {
176
217
  const query = rawQuery.trim();
177
218
  if (!query)
@@ -346,7 +387,14 @@ export class BrowserAutomationService {
346
387
  async screenshot(options) {
347
388
  return this.snapshot({ ...options, screenshot: true });
348
389
  }
390
+ /** True when this service runs a visible browser instead of the background default. */
391
+ get visibleBrowser() {
392
+ return this.options.headless === false;
393
+ }
349
394
  async focus() {
395
+ if (!this.visibleBrowser) {
396
+ throw new BrowserAutomationError("HEADLESS_NO_WINDOW", "浏览器正在后台运行(LOOP_BROWSER_HEADLESS=1),没有可聚焦的窗口;取消该设置并重启 Console 可恢复可见窗口,或在右侧面板预览当前页面");
397
+ }
350
398
  const page = await this.page();
351
399
  await page.bringToFront();
352
400
  return { ok: true };
@@ -358,7 +406,7 @@ export class BrowserAutomationService {
358
406
  }
359
407
  async pick(input) {
360
408
  if (this.options.headless === true)
361
- throw new BrowserAutomationError("PICK_UNAVAILABLE", "human picking requires the visible external browser");
409
+ throw new BrowserAutomationError("PICK_UNAVAILABLE", "human picking requires the visible external browser; unset LOOP_BROWSER_HEADLESS (or set it to 0) and restart the console to launch one");
362
410
  const page = await this.page();
363
411
  await page.bringToFront();
364
412
  const element = await pickBrowserElement(page, input.message, clamp(input.timeoutMs ?? 30_000, 1, 60_000), this.operations.current?.signal).catch((error) => {
@@ -480,6 +528,7 @@ export class BrowserAutomationService {
480
528
  if (this.disposed)
481
529
  return;
482
530
  this.disposed = true;
531
+ await this.stopMirror().catch(() => undefined);
483
532
  const pending = this.browserPromise;
484
533
  this.browserPromise = undefined;
485
534
  this.diagnostics.reset();
@@ -581,6 +630,9 @@ export class BrowserAutomationService {
581
630
  this.dialogQueue.length = 0;
582
631
  this.downloadQueue.length = 0;
583
632
  this.tabs.clear();
633
+ this.mirrorPage = undefined;
634
+ this.mirrorSession = undefined;
635
+ this.mirrorFrame = undefined;
584
636
  }
585
637
  });
586
638
  }
@@ -628,9 +680,53 @@ export class BrowserAutomationService {
628
680
  this.tabs.delete(id);
629
681
  if (this.activePage === page)
630
682
  this.activePage = undefined;
683
+ if (this.mirrorPage === page) {
684
+ this.mirrorPage = undefined;
685
+ this.mirrorSession = undefined;
686
+ this.mirrorFrame = undefined;
687
+ }
631
688
  });
632
689
  return id;
633
690
  }
691
+ /** Attaches a CDP screencast to the page, replacing any previous session. */
692
+ async startMirror(page) {
693
+ await this.stopMirror();
694
+ this.mirrorFrame = undefined;
695
+ try {
696
+ const session = await page.context().newCDPSession(page);
697
+ session.on("Page.screencastFrame", (frame) => {
698
+ this.mirrorSeq += 1;
699
+ this.mirrorFrame = { data: frame.data };
700
+ void session.send("Page.screencastFrameAck", { sessionId: frame.sessionId }).catch(() => undefined);
701
+ });
702
+ await session.send("Page.startScreencast", {
703
+ format: "jpeg",
704
+ quality: 85,
705
+ maxWidth: BROWSER_VIEWPORT_WIDTH,
706
+ maxHeight: BROWSER_VIEWPORT_HEIGHT,
707
+ everyNthFrame: 1,
708
+ });
709
+ this.mirrorSession = session;
710
+ this.mirrorPage = page;
711
+ }
712
+ catch {
713
+ await this.stopMirror().catch(() => undefined);
714
+ }
715
+ }
716
+ async stopMirror() {
717
+ const session = this.mirrorSession;
718
+ this.mirrorSession = undefined;
719
+ this.mirrorPage = undefined;
720
+ if (!session)
721
+ return;
722
+ try {
723
+ await session.send("Page.stopScreencast");
724
+ }
725
+ catch {
726
+ // The session may already be gone with its page or context.
727
+ }
728
+ await session.detach().catch(() => undefined);
729
+ }
634
730
  dialogFor(page) {
635
731
  const index = this.dialogQueue.findIndex((entry) => entry.page === page);
636
732
  const dialog = index >= 0 ? this.dialogQueue[index]?.dialog : undefined;
@@ -729,17 +825,30 @@ function emptyBrowserState() {
729
825
  ariaSnapshot: "",
730
826
  };
731
827
  }
732
- function chromiumLaunchOptions(headless) {
828
+ /** Linux GitHub runners (and similar CI) have no usable Chromium sandbox. */
829
+ export function chromiumSandboxEnabled(env = process.env) {
830
+ return env.GITHUB_ACTIONS !== "true" && env.CI !== "true";
831
+ }
832
+ export function chromiumLaunchOptions(headless, env = process.env) {
733
833
  return {
734
834
  headless: headless ?? false,
735
835
  viewport: undefined,
736
836
  acceptDownloads: true,
737
837
  locale: "zh-CN",
738
838
  timezoneId: "Asia/Shanghai",
739
- args: ["--start-maximized", "--disable-blink-features=AutomationControlled"],
839
+ // Desktop: drop Playwright's implicit --no-sandbox so Chrome does not show
840
+ // the "unsupported command-line flag" infobar. CI Linux has no usable
841
+ // user-namespace sandbox, so keep Playwright's --no-sandbox there.
842
+ chromiumSandbox: chromiumSandboxEnabled(env),
843
+ args: ["--disable-blink-features=AutomationControlled"],
740
844
  ignoreDefaultArgs: ["--enable-automation"],
741
845
  };
742
846
  }
847
+ /** LOOP_BROWSER_HEADLESS=1/true/on runs the browser in the background (headless). */
848
+ export function browserHeadlessEnabled(env = process.env) {
849
+ const raw = env.LOOP_BROWSER_HEADLESS?.trim().toLowerCase();
850
+ return raw === "1" || raw === "true" || raw === "on";
851
+ }
743
852
  /**
744
853
  * Bounded settle after a committed navigation. `domcontentloaded` fires before
745
854
  * late client-side redirects (for example an engine that reroutes to a regional
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * HTTP API for the workspace-controlled external browser. The sidebar iframe
3
3
  * is an independent preview; these endpoints operate the Playwright page.
4
+ * `GET panel/frame` additionally serves passive screencast frames so the
5
+ * panel can mirror the shared window (same context, login state included).
4
6
  */
5
7
  import { embeddabilityOf, extractFrameAncestors, normalizeBrowserUrl, } from "./browser-policy.js";
6
8
  function workspaceIdOf(req) {
@@ -170,6 +172,25 @@ export async function handleBrowserRequest(req, res, deps, pathname) {
170
172
  clearTimeout(timer);
171
173
  }
172
174
  }
175
+ if (method === "GET" && sub === "panel" && parts[1] === "frame") {
176
+ const scope = requireScope(req, deps);
177
+ if (!scope.ok) {
178
+ sendJson(res, scope.status, {
179
+ ok: false,
180
+ error: { code: scope.code, message: scope.message },
181
+ });
182
+ return true;
183
+ }
184
+ if (!deps.automation) {
185
+ sendJson(res, 200, { ok: true, data: { running: false, changed: false, seq: 0 } });
186
+ return true;
187
+ }
188
+ const sinceRaw = new URL(req.url ?? "/", "http://localhost").searchParams.get("since");
189
+ const parsed = sinceRaw === null ? 0 : Number(sinceRaw);
190
+ const frame = await deps.automation.panelFrame(Number.isFinite(parsed) ? parsed : 0);
191
+ sendJson(res, 200, { ok: true, data: frame });
192
+ return true;
193
+ }
173
194
  if (method === "POST" && parts.length >= 1) {
174
195
  const scope = requireScope(req, deps);
175
196
  if (!scope.ok) {
@@ -502,8 +502,8 @@ const DEFAULT_PERSIST_DEBOUNCE_MS = 250;
502
502
  * interim flush is forced, so a fast stream never widens the loss window
503
503
  * unboundedly even if the `at` clock stalls. */
504
504
  const DEFAULT_PERSIST_MAX_PENDING_UPDATES = 250;
505
- /** Conservative per-event scale for the byte pre-check gate (UTF-8 multibyte ≤ 4 bytes/char × pretty-print expansion). */
506
- const BYTE_GATE_SCALE = 12;
505
+ /** Conservative per-event scale for the byte pre-check gate (UTF-8 multibyte ≤ 4 bytes/char). Persist is compact JSON, so this is no longer multiplied by pretty-print expansion. */
506
+ const BYTE_GATE_SCALE = 4;
507
507
  /** Fixed envelope slack (schema keys, turnCount, nextSeq, minSeq, turns) for the byte pre-check gate. */
508
508
  const BYTE_GATE_SLACK = 8192;
509
509
  /** Legacy epoch for event files written before M5.1 (no epoch field). */
@@ -704,7 +704,8 @@ export function createChatEventStore(options) {
704
704
  const maxBytes = options?.maxBytesPerSession ?? DEFAULT_MAX_BYTES;
705
705
  const coalesceWindowMs = options?.coalesceWindowMs ?? DEFAULT_COALESCE_WINDOW_MS;
706
706
  const eventEpoch = options?.epoch?.trim() || LEGACY_EPOCH;
707
- const persistWriter = options?.persistWriter ?? writeSecureJsonSync;
707
+ const persistWriter = options?.persistWriter ??
708
+ ((file, payload) => writeSecureJsonSync(file, payload, { pretty: false }));
708
709
  const persistDebounceMs = options?.persistDebounceMs ?? DEFAULT_PERSIST_DEBOUNCE_MS;
709
710
  const persistMaxPendingUpdates = options?.persistMaxPendingUpdates ?? DEFAULT_PERSIST_MAX_PENDING_UPDATES;
710
711
  const persistenceDir = options?.appData?.chatEvents
@@ -807,10 +808,10 @@ export function createChatEventStore(options) {
807
808
  epoch: ring.epoch,
808
809
  };
809
810
  }
810
- /** Serialized exactly as `writeSecureJsonSync` writes it, so byte accounting
811
- * (D4) and the on-disk bytes use the same formatting. */
811
+ /** Serialized exactly as compact `writeSecureJsonSync` writes it, so byte
812
+ * accounting (D4) and the on-disk bytes use the same formatting. */
812
813
  function serializeRingForPersist(sessionId, ring) {
813
- return `${JSON.stringify(ringPayload(sessionId, ring), null, 2)}\n`;
814
+ return `${JSON.stringify(ringPayload(sessionId, ring))}\n`;
814
815
  }
815
816
  function bytesOfRing(sessionId, ring) {
816
817
  return Buffer.byteLength(serializeRingForPersist(sessionId, ring), "utf8");
@@ -261,6 +261,52 @@ export function deriveContextInsights(events, options = {}) {
261
261
  const browserMessages = [];
262
262
  const browserInjected = [];
263
263
  const browserToolResults = [];
264
+ const liveNodes = [];
265
+ /** Nodes removed by a prune/compaction, newest last, for the prune card. */
266
+ const archivedNodes = [];
267
+ /** Remove the ledger nodes whose seq the removal claims; deduct slices and browser rows. */
268
+ function claimNodes(claimed, removedAtSeq) {
269
+ if (claimed.size === 0)
270
+ return [];
271
+ const removedIds = [];
272
+ const kept = [];
273
+ for (const node of liveNodes) {
274
+ if (claimed.has(node.seq)) {
275
+ const slice = sliceOf(composition, node.category);
276
+ slice.tokens = Math.max(0, slice.tokens - node.tokens);
277
+ slice.items = Math.max(0, slice.items - 1);
278
+ archivedNodes.push({ ...node, removedAt: removedAtSeq });
279
+ removedIds.push(node.element.id);
280
+ }
281
+ else {
282
+ kept.push(node);
283
+ }
284
+ }
285
+ if (removedIds.length > 0) {
286
+ liveNodes.length = 0;
287
+ liveNodes.push(...kept);
288
+ const drop = (list) => {
289
+ for (let i = list.length - 1; i >= 0; i -= 1) {
290
+ const row = list[i];
291
+ if (row && claimed.has(row.seq))
292
+ list.splice(i, 1);
293
+ }
294
+ };
295
+ drop(browserMessages);
296
+ drop(browserToolResults);
297
+ drop(browserInjected);
298
+ }
299
+ return removedIds;
300
+ }
301
+ /** Parse "<sessionId>:<seq>" entry ids into a keep-from seq anchor. */
302
+ function entrySeqOf(entryId) {
303
+ if (typeof entryId !== "string" || !entryId)
304
+ return undefined;
305
+ const idx = entryId.lastIndexOf(":");
306
+ const tail = idx >= 0 ? entryId.slice(idx + 1) : entryId;
307
+ const seq = Number.parseInt(tail, 10);
308
+ return Number.isInteger(seq) && seq >= 0 ? seq : undefined;
309
+ }
264
310
  if (options.systemPromptPreview) {
265
311
  addSlice(composition, "system", estimateTokensFromText(options.systemPromptPreview));
266
312
  }
@@ -306,14 +352,16 @@ export function deriveContextInsights(events, options = {}) {
306
352
  if (text) {
307
353
  const tokens = estimateTokensFromText(text);
308
354
  addSlice(composition, "user", tokens);
309
- browserMessages.push({
355
+ const element = {
310
356
  id: `user:${event.seq}`,
311
357
  label: "用户",
312
358
  tokens,
313
359
  preview: text,
314
360
  seq: event.seq,
315
361
  at: event.at,
316
- });
362
+ };
363
+ browserMessages.push(element);
364
+ liveNodes.push({ seq: event.seq, tokens, category: "user", element });
317
365
  if (!turnUserBrief.has(event.turnId)) {
318
366
  turnUserBrief.set(event.turnId, text.slice(0, 200));
319
367
  }
@@ -338,14 +386,16 @@ export function deriveContextInsights(events, options = {}) {
338
386
  if (text) {
339
387
  const tokens = estimateTokensFromText(text);
340
388
  addSlice(composition, "assistant", tokens);
341
- browserMessages.push({
389
+ const element = {
342
390
  id: `assistant:${event.seq}`,
343
391
  label: thinking ? "助手(含思考)" : "助手",
344
392
  tokens,
345
393
  preview: text,
346
394
  seq: event.seq,
347
395
  at: event.at,
348
- });
396
+ };
397
+ browserMessages.push(element);
398
+ liveNodes.push({ seq: event.seq, tokens, category: "assistant", element });
349
399
  }
350
400
  const toolsCalled = stepToolsCalled.get(event.turnId) ?? [];
351
401
  const recorded = asRecord(data.timing);
@@ -474,7 +524,7 @@ export function deriveContextInsights(events, options = {}) {
474
524
  if (resultPreview) {
475
525
  const tokens = estimateTokensFromText(resultPreview);
476
526
  addSlice(composition, "toolResults", tokens);
477
- browserToolResults.push({
527
+ const element = {
478
528
  id: `result:${event.seq}`,
479
529
  label: typeof data.toolName === "string" && data.toolName
480
530
  ? data.toolName
@@ -484,7 +534,9 @@ export function deriveContextInsights(events, options = {}) {
484
534
  status: data.isError === true ? "error" : "ok",
485
535
  seq: event.seq,
486
536
  at: event.at,
487
- });
537
+ };
538
+ browserToolResults.push(element);
539
+ liveNodes.push({ seq: event.seq, tokens, category: "toolResults", element });
488
540
  }
489
541
  break;
490
542
  }
@@ -531,6 +583,30 @@ export function deriveContextInsights(events, options = {}) {
531
583
  const after = num(data.estimatedTokensAfter);
532
584
  const reclaimed = before !== undefined && after !== undefined ? before - after : null;
533
585
  const pruned = data.pruned === true || data.kind === "prune";
586
+ // --- Exact removal (dsh-context parity) ---
587
+ // A prune/compaction claims live nodes either by explicit seq list
588
+ // (shadowedSeqs) or by a keep-from anchor (firstKeptEntryId = the
589
+ // first ring entry that survives). Claim exactly those nodes from
590
+ // the ledger: deduct slices, drop browser rows, archive the nodes.
591
+ const explicitSeqs = Array.isArray(data.shadowedSeqs)
592
+ ? data.shadowedSeqs.filter((value) => typeof value === "number" && Number.isInteger(value) && value >= 0)
593
+ : undefined;
594
+ const keepFromSeq = entrySeqOf(data.firstKeptEntryId);
595
+ let removedIds = [];
596
+ let removalBasis;
597
+ if (explicitSeqs && explicitSeqs.length > 0) {
598
+ removedIds = claimNodes(new Set(explicitSeqs), event.seq);
599
+ removalBasis = "seqs";
600
+ }
601
+ else if (keepFromSeq !== undefined && liveNodes.length > 0) {
602
+ const rangeSeqs = new Set();
603
+ for (const node of liveNodes) {
604
+ if (node.seq < keepFromSeq)
605
+ rangeSeqs.add(node.seq);
606
+ }
607
+ removedIds = claimNodes(rangeSeqs, event.seq);
608
+ removalBasis = "range";
609
+ }
534
610
  contextEvents.push({
535
611
  seq: event.seq,
536
612
  at: event.at,
@@ -548,25 +624,14 @@ export function deriveContextInsights(events, options = {}) {
548
624
  ...(Array.isArray(data.shadowedSeqs) ? { shadowedItems: data.shadowedSeqs.length } : {}),
549
625
  ...(typeof data.reason === "string" ? { reason: data.reason } : {}),
550
626
  ...(pruned ? { pruned: true } : {}),
627
+ ...(removedIds.length > 0 ? { removedIds } : {}),
628
+ ...(removalBasis !== undefined ? { removalBasis } : {}),
551
629
  detail: boundedText(asRecord(data.details)?.text, 300),
552
630
  });
553
631
  const lastStep = trend.at(-1);
554
632
  if (lastStep && event.kind !== "compaction_start") {
555
633
  lastStep.eventMark = pruned ? "prune" : "compact";
556
634
  }
557
- if (reclaimed !== null && reclaimed > 0) {
558
- const userSlice = sliceOf(composition, "user");
559
- const assistantSlice = sliceOf(composition, "assistant");
560
- const toolSlice = sliceOf(composition, "toolResults");
561
- let remaining = reclaimed;
562
- for (const slice of [userSlice, assistantSlice, toolSlice]) {
563
- const take = Math.min(slice.tokens, remaining);
564
- slice.tokens -= take;
565
- remaining -= take;
566
- if (remaining <= 0)
567
- break;
568
- }
569
- }
570
635
  break;
571
636
  }
572
637
  default:
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Pet affinity + treat economy — ported from @linxin666/dsh-pet
3
+ * affinity.ts + treats.ts. Pure, clock-injected; persistence lives in the
4
+ * service. Ranks use plain ASCII glyphs (repo bans emoji characters).
5
+ * @module loop-agent/console/chat/pet/pet-affinity
6
+ */
7
+ import { BUILTIN_PET_REMARKS, builtinPetRemark, } from "./pet-remarks.js";
8
+ export const PET_AFFINITY_MAX = 999_999_999;
9
+ export const PET_AFFINITY_RANKS = [
10
+ { min: 0, name: "一粒火星", glyph: "*" },
11
+ { min: 25, name: "小火苗", glyph: "**" },
12
+ { min: 50, name: "炉边伙伴", glyph: "***" },
13
+ { min: 80, name: "炽热搭档", glyph: "****" },
14
+ { min: 200, name: "烈焰挚友", glyph: "*****" },
15
+ { min: 500, name: "篝火达人", glyph: "******" },
16
+ { min: 2_000, name: "火焰大师", glyph: "*******" },
17
+ { min: 10_000, name: "燎原之势", glyph: "********" },
18
+ { min: 100_000, name: "恒星之火", glyph: "*********" },
19
+ ];
20
+ export const defaultPetAffinityConfig = {
21
+ turnReward: 1,
22
+ petReward: 1,
23
+ petCooldownMs: 10_000,
24
+ feedReward: 5,
25
+ feedCooldownMs: 30_000,
26
+ };
27
+ export function emptyPetAffinity() {
28
+ return {
29
+ points: 0,
30
+ lastPetAt: 0,
31
+ lastFeedAt: 0,
32
+ pets: 0,
33
+ feeds: 0,
34
+ petRejects: 0,
35
+ feedRejects: 0,
36
+ turns: 0,
37
+ };
38
+ }
39
+ export function petRankOf(points) {
40
+ let rank = PET_AFFINITY_RANKS[0];
41
+ for (const candidate of PET_AFFINITY_RANKS) {
42
+ if (points >= candidate.min)
43
+ rank = candidate;
44
+ }
45
+ return rank;
46
+ }
47
+ export function petAffinityViewOf(state, nowMs, config = defaultPetAffinityConfig) {
48
+ const rank = petRankOf(state.points);
49
+ return {
50
+ points: state.points,
51
+ rank: rank.name,
52
+ rankGlyph: rank.glyph,
53
+ pets: state.pets,
54
+ feeds: state.feeds,
55
+ turns: state.turns,
56
+ petCooldown: nowMs - state.lastPetAt < config.petCooldownMs,
57
+ feedCooldown: nowMs - state.lastFeedAt < config.feedCooldownMs,
58
+ };
59
+ }
60
+ function clampPoints(points) {
61
+ return Math.min(PET_AFFINITY_MAX, Math.max(0, points));
62
+ }
63
+ /** Deterministic round-robin reaction from a persisted counter. */
64
+ function countedRemark(kind, count) {
65
+ const pool = BUILTIN_PET_REMARKS[kind];
66
+ return pool[Math.max(0, Math.floor(count)) % pool.length] ?? builtinPetRemark(kind);
67
+ }
68
+ /**
69
+ * Apply one interaction to a copy of the state. Cooldowns only apply once
70
+ * the pet has been interacted with at least once (last*At === 0 = never).
71
+ */
72
+ export function applyPetInteraction(state, kind, nowMs, config = defaultPetAffinityConfig) {
73
+ const next = { ...state };
74
+ if (kind === "pet") {
75
+ if (state.lastPetAt !== 0 &&
76
+ nowMs - state.lastPetAt < config.petCooldownMs) {
77
+ next.petRejects += 1;
78
+ return {
79
+ affinity: next,
80
+ delta: 0,
81
+ reaction: countedRemark("petCooldown", state.petRejects),
82
+ accepted: false,
83
+ };
84
+ }
85
+ next.lastPetAt = nowMs;
86
+ next.pets += 1;
87
+ next.points = clampPoints(state.points + config.petReward);
88
+ return {
89
+ affinity: next,
90
+ delta: config.petReward,
91
+ reaction: countedRemark("pet", state.pets),
92
+ accepted: true,
93
+ };
94
+ }
95
+ if (kind === "feed") {
96
+ if (state.lastFeedAt !== 0 &&
97
+ nowMs - state.lastFeedAt < config.feedCooldownMs) {
98
+ next.feedRejects += 1;
99
+ return {
100
+ affinity: next,
101
+ delta: 0,
102
+ reaction: countedRemark("feedCooldown", state.feedRejects),
103
+ accepted: false,
104
+ };
105
+ }
106
+ next.lastFeedAt = nowMs;
107
+ next.feeds += 1;
108
+ next.points = clampPoints(state.points + config.feedReward);
109
+ return {
110
+ affinity: next,
111
+ delta: config.feedReward,
112
+ reaction: countedRemark("feed", state.feeds),
113
+ accepted: true,
114
+ };
115
+ }
116
+ return { affinity: state, delta: 0, reaction: "", accepted: false };
117
+ }
118
+ /** Reward one completed turn (host calls this on phase done). */
119
+ export function applyPetTurnReward(state, config = defaultPetAffinityConfig) {
120
+ const next = { ...state };
121
+ next.turns += 1;
122
+ next.points = clampPoints(state.points + config.turnReward);
123
+ return next;
124
+ }
125
+ export const defaultPetTreatConfig = {
126
+ turnsPerTreat: 30,
127
+ timeTreatMs: 300 * 60_000,
128
+ maxTreats: 20,
129
+ };
130
+ export function emptyPetTreatLedger() {
131
+ return { treats: 0, lastTreatGrantAt: 0, turnsAtLastTreatGrant: 0 };
132
+ }
133
+ function capTreats(treats, max) {
134
+ return Math.min(max, Math.max(0, treats));
135
+ }
136
+ /**
137
+ * Settle treat grants from both sources (work output + wall-clock time).
138
+ * Lazy settlement from persisted anchors — no timer, no drift. A zero-gain
139
+ * first settlement still writes the time anchor so the clock can start.
140
+ */
141
+ export function settlePetTreatGrants(ledger, turns, nowMs, config = defaultPetTreatConfig) {
142
+ const turnDelta = Math.max(0, turns - ledger.turnsAtLastTreatGrant);
143
+ const workGrants = Math.floor(turnDelta / config.turnsPerTreat);
144
+ const timeAnchor = ledger.lastTreatGrantAt === 0 ? nowMs : ledger.lastTreatGrantAt;
145
+ const timeGrants = Math.floor(Math.max(0, nowMs - timeAnchor) / config.timeTreatMs);
146
+ const gained = workGrants + timeGrants;
147
+ if (gained <= 0) {
148
+ if (ledger.lastTreatGrantAt === 0) {
149
+ return { ledger: { ...ledger, lastTreatGrantAt: nowMs }, gained: 0 };
150
+ }
151
+ return { ledger, gained: 0 };
152
+ }
153
+ return {
154
+ ledger: {
155
+ treats: capTreats(ledger.treats + gained, config.maxTreats),
156
+ lastTreatGrantAt: timeGrants > 0 ? timeAnchor + timeGrants * config.timeTreatMs : timeAnchor,
157
+ turnsAtLastTreatGrant: workGrants > 0
158
+ ? turns - (turnDelta % config.turnsPerTreat)
159
+ : ledger.turnsAtLastTreatGrant,
160
+ },
161
+ gained,
162
+ };
163
+ }
164
+ /** Consume one treat for a feed; refuses when the stock is empty. */
165
+ export function consumePetTreat(ledger) {
166
+ if (ledger.treats <= 0)
167
+ return { ok: false };
168
+ return { ok: true, ledger: { ...ledger, treats: ledger.treats - 1 } };
169
+ }