@tea-agent/loop-agent 0.44.0-next.12 → 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 (188) hide show
  1. package/CHANGELOG.md +27 -2
  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 +686 -330
  10. package/dist/commands/operator-guide.js +99 -0
  11. package/dist/commands/operator.js +16 -3
  12. package/dist/executors/dag-pi-executor.js +11 -6
  13. package/dist/executors/pi-sdk-executor.js +3 -1
  14. package/dist/executors/pi-writer-tool-policy.js +60 -4
  15. package/dist/executors/playwright-cli-launcher.js +31 -45
  16. package/dist/executors/shell-write-guard.js +24 -67
  17. package/dist/governance/exec-plans.js +10 -1
  18. package/dist/governance/harness.js +2 -7
  19. package/dist/governance/manifest-types.js +28 -9
  20. package/dist/governance/project-capability-scanner.js +263 -0
  21. package/dist/governance/project-config.js +96 -0
  22. package/dist/governance/resource-catalog.js +151 -0
  23. package/dist/infrastructure/console/app-data.js +6 -2
  24. package/dist/shared/operator/capabilities.js +2 -2
  25. package/dist/shared/package-metadata.js +23 -7
  26. package/dist/shared/pi-context-pressure/extension.js +23 -0
  27. package/dist/shared/pi-context-pressure/index.js +1 -0
  28. package/dist/shared/pi-context-pressure/telemetry.js +4 -0
  29. package/dist/shared/pi-context-pressure/tool-result-pruner.js +98 -0
  30. package/dist/shared/update/npm-client.js +49 -1
  31. package/dist/task/config-types.js +4 -2
  32. package/dist/task/source-prepare/build-draft.js +1 -0
  33. package/dist/task/source-prepare/path-policy.js +17 -7
  34. package/dist/worker/cli.js +3 -5
  35. package/dist/worker/console/chat/browser-automation.js +455 -41
  36. package/dist/worker/console/chat/browser-content.js +74 -0
  37. package/dist/worker/console/chat/browser-diagnostics.js +250 -0
  38. package/dist/worker/console/chat/browser-evaluation.js +58 -0
  39. package/dist/worker/console/chat/browser-operation-queue.js +19 -0
  40. package/dist/worker/console/chat/browser-pick.js +113 -0
  41. package/dist/worker/console/chat/browser-routes.js +54 -5
  42. package/dist/worker/console/chat/browser-screenshot-result.js +47 -0
  43. package/dist/worker/console/chat/browser-storage.js +89 -0
  44. package/dist/worker/console/chat/chat-event-store.js +7 -6
  45. package/dist/worker/console/chat/context-insights.js +84 -19
  46. package/dist/worker/console/chat/pet/pet-affinity.js +169 -0
  47. package/dist/worker/console/chat/pet/pet-persist.js +109 -0
  48. package/dist/worker/console/chat/pet/pet-registry.js +142 -0
  49. package/dist/worker/console/chat/pet/pet-remarks.js +48 -0
  50. package/dist/worker/console/chat/pet/pet-routes.js +125 -0
  51. package/dist/worker/console/chat/pet/pet-service.js +262 -0
  52. package/dist/worker/console/chat/pet/pet-state.js +99 -0
  53. package/dist/worker/console/chat/pi-mode-loop-isolation.js +17 -2
  54. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +125 -174
  55. package/dist/worker/console/chat/pi-runtime.js +8 -3
  56. package/dist/worker/console/chat/repo-browser.js +80 -2
  57. package/dist/worker/console/chat/repo-preview-kind.js +155 -0
  58. package/dist/worker/console/chat/routes.js +43 -7
  59. package/dist/worker/console/chat/scheduled-goal-request.js +14 -6
  60. package/dist/worker/console/chat/session-mode.js +1 -1
  61. package/dist/worker/console/chat/session-store.js +24 -5
  62. package/dist/worker/console/chat/tools.js +12 -0
  63. package/dist/worker/console/console-shutdown.js +34 -0
  64. package/dist/worker/console/server.js +2 -1
  65. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-r1NAPU_5.js → abnfDiagram-N423BO3Z-DASc69AM.js} +1 -1
  66. package/dist/worker/console/static/assets/{arc-dT5T4sVL.js → arc-h09p-LAc.js} +1 -1
  67. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP_cOq46.js → architectureDiagram-T3A2C74G-CZ3dtLkh.js} +1 -1
  68. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-sToEZxEf.js → blockDiagram-VBNYF7ZC-DMrRhMXG.js} +1 -1
  69. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DSxwwi2m.js → c4Diagram-5PPSVZJV-DQxisHV6.js} +1 -1
  70. package/dist/worker/console/static/assets/channel-D1zgKZfg.js +1 -0
  71. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B9fh0IkF.js → chunk-2GRJ4B5K-CefVN9zh.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CAUNl-A6.js → chunk-2Q5K7J3B-DmR1Koq2.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BomeqbNX.js → chunk-5RXB4S5H-NlzrSW7L.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BNAuTOLE.js → chunk-5VM5RSS4-DfelJv-0.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DfzjRD5y.js → chunk-6Q2QTUOP-BK6RFy58.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3uE-yc2.js → chunk-GF5L2VYU-BAeh9lz4.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-C3sQMMYr.js → chunk-JWPE2WC7-ChPS5WzG.js} +1 -1
  78. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D_AtEYQL.js → chunk-KBJHAD2P-BoVe5IM6.js} +1 -1
  79. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DW4TwhKv.js → chunk-RYQCIY6F-C4q_0Lw9.js} +1 -1
  80. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CTQDmJcp.js → chunk-XXDRQBXY-Bb9TvjeY.js} +1 -1
  81. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CXNNsY2M.js +1 -0
  82. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CXNNsY2M.js +1 -0
  83. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-iMqPuBsL.js → cose-bilkent-JH36ORCC-DmNnL8Ff.js} +1 -1
  84. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DktIuDWs.js → cynefin-VYW2F7L2-Contvx58.js} +1 -1
  85. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BgVnjyEs.js → cynefinDiagram-MW4NZA55-Cigq7cjk.js} +1 -1
  86. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CR25E4bu.js → dagre-VZM6K2ZE-dEbC0nxb.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CPM2h5HY.js → diagram-7IWD3JNH-CzIztESV.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DqF0x2g1.js → diagram-B4RE2ZJO-Bo14zbaY.js} +1 -1
  89. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-G8BYp2eb.js → diagram-LBJQPF4R-C4xUyil6.js} +1 -1
  90. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BbC4kT_D.js → diagram-Q27KOJAE-CaBY_uYa.js} +1 -1
  91. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CKZwZ-G5.js → diagram-UB23O5K3-ClUXUeiu.js} +1 -1
  92. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-kJDmheia.js → ebnfDiagram-BXEA7PRR-BbGCgjC6.js} +1 -1
  93. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DFTVHyxB.js → erDiagram-JOGREHBK-Dr-AcwSl.js} +1 -1
  94. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqdN5_os.js → flowDiagram-UKHOOZJN-BG8Gcfyc.js} +1 -1
  95. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-SYDjVakh.js → ganttDiagram-PKOTCBZU-CFVaPs5r.js} +1 -1
  96. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js → gitGraphDiagram-DS77QQ5N-ChJ8pm-O.js} +1 -1
  97. package/dist/worker/console/static/assets/index-B_5GhVIO.js +488 -0
  98. package/dist/worker/console/static/assets/index-C7YMBluX.css +1 -0
  99. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DIeAmJ45.js → infoDiagram-6WML65LV-C5J9-uCZ.js} +1 -1
  100. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-oikqUAlo.js → ishikawaDiagram-WSZJBQD7-BcfjqzDv.js} +1 -1
  101. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-mKEF_xYT.js → journeyDiagram-NVQOT4AX-BzzCO-rD.js} +1 -1
  102. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BdWJHPnN.js → kanban-definition-27J2QSJJ-NCHUZ46h.js} +1 -1
  103. package/dist/worker/console/static/assets/{linear-DGRlieMH.js → linear-BKZlWCUU.js} +1 -1
  104. package/dist/worker/console/static/assets/{mermaid.core-4P9yPC9c.js → mermaid.core-DbTaQjFE.js} +5 -5
  105. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-5PTSkos6.js → mindmap-definition-FAOFIHXS-CroYl5xQ.js} +1 -1
  106. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DONIFpG5.js → pegDiagram-VL7TDLO6-DgMBx-9k.js} +1 -1
  107. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D_Z4S70M.js → pieDiagram-7S7Q4E2Y-CyWDmbq1.js} +1 -1
  108. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CAsjh8iY.js → quadrantDiagram-CIZ2JOQS-BgGEOqSP.js} +1 -1
  109. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Cx4Dd6ac.js → railroadDiagram-AXF67PYL-BMZ7oeeV.js} +1 -1
  110. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-cQb7mwyH.js → requirementDiagram-LRYGKXZP-DxqyGhcc.js} +1 -1
  111. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cd-DhUdf.js → sankeyDiagram-W5VNT64P-DnSvNCG8.js} +1 -1
  112. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-AlKYRgIk.js → sequenceDiagram-SI44F4Z6-BiZh6f-R.js} +1 -1
  113. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CUy_ET4e.js → sizeCapture-X5ZJPWSS-BKXSI8WB.js} +1 -1
  114. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CTuVe6Sa.js → stateDiagram-OKZ733FA-CfSgoYFm.js} +1 -1
  115. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DgHi3vjb.js +1 -0
  116. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ChFLJb-v.js → swimlanes-SLNWSIFB-CyGADcHe.js} +2 -2
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bgvdt1Y0.js +8 -0
  118. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DZZeiAPr.js → timeline-definition-Z64GVDOM-HmPaSPOK.js} +1 -1
  119. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-t4O91cZU.js → vennDiagram-T6HMQDX7-VA2VBbez.js} +1 -1
  120. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CV5qGHZu.js → wardleyDiagram-T6FBY63Y-BxjiBPZa.js} +1 -1
  121. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DyXbytUI.js → xychartDiagram-ELKLHX3M--hUY-qFj.js} +1 -1
  122. package/dist/worker/console/static/index.html +2 -2
  123. package/dist/worker/console/static/pets/ember/pet.json +167 -0
  124. package/dist/worker/console/static/pets/ember/spritesheet.png +0 -0
  125. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +4 -3
  126. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +57 -9
  127. package/dist/worker/console/static-src/operator-chat/use-stat-dialog.js +95 -0
  128. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +6 -0
  129. package/dist/worker/console/workspace-context.js +15 -0
  130. package/dist/worker/console/workspace-initialization.js +25 -16
  131. package/dist/worker/delivery/package.js +4 -1
  132. package/dist/worker/materialize/harness-task-lineage.js +4 -2
  133. package/dist/worker/observe/static/console-theme.css +33 -29
  134. package/dist/worker/observe/static/operator-chrome.js +1 -42
  135. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +3 -1
  136. package/dist/workflows/dag/backend-test-layout.js +27 -7
  137. package/dist/workflows/dag/backend-test-markdown-workflow.js +3 -1
  138. package/dist/workflows/dag/backend-test-pytest-collection.js +4 -1
  139. package/dist/workflows/dag/final-verification.js +45 -10
  140. package/dist/workflows/dag/frontend-test-case-checklist.js +12 -3
  141. package/dist/workflows/dag/frontend-test-environment-probe.js +0 -2
  142. package/dist/workflows/dag/frontend-test-layout.js +41 -12
  143. package/dist/workflows/dag/hybrid/sources.js +35 -16
  144. package/dist/workflows/dag/hybrid/templates/backend-test.js +70 -55
  145. package/dist/workflows/dag/hybrid/templates/frontend-test.js +62 -41
  146. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +6 -6
  147. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +5 -5
  148. package/dist/workflows/dag/skill-instructions.js +102 -32
  149. package/dist/workflows/dag/skill-snapshot.js +3 -0
  150. package/dist/workflows/dag/skills.js +7 -7
  151. package/dist/workflows/dag/test-artifact-namespace.js +73 -0
  152. package/dist/workflows/dag/validate.js +24 -5
  153. package/dist/workflows/dag/workspace-checkpoint.js +13 -1
  154. package/docs/architecture/runtime-boundaries.md +28 -22
  155. package/docs/init-surface.manifest.json +43 -295
  156. package/docs/templates/README.md +4 -4
  157. package/docs/templates/agent-dag.base.json +9 -9
  158. package/docs/templates/agent-dag.final-verification.json +6 -6
  159. package/docs/templates/agent-dag.supervised-implementation.json +12 -12
  160. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -3
  161. package/docs/templates/backend-test-dag.json +708 -708
  162. package/docs/templates/backend-test-dag.review-cases.prompt.md +3 -3
  163. package/docs/templates/frontend-implementation-dag.json +2 -2
  164. package/docs/templates/frontend-test-case-checklist.md +2 -2
  165. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +7 -7
  166. package/docs/templates/frontend-test-dag.json +48 -39
  167. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +2 -2
  168. package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
  169. package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
  170. package/docs/templates/hybrid-dag.json +9 -9
  171. package/docs/templates/knowledge-sync-dag.json +8 -8
  172. package/package.json +12 -3
  173. package/resource-catalog.v1.json +1973 -0
  174. package/resources/agent-bridge/loop-agent/SKILL.md +40 -0
  175. package/resources/init-history/legacy-surface.v1.json +33 -0
  176. package/skills/fe-test-ui-scout/SKILL.md +4 -4
  177. package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
  178. package/skills/init-update/SKILL.md +34 -0
  179. package/skills/init-update/agents/openai.yaml +6 -0
  180. package/skills/playwright-cli/SKILL.md +1 -1
  181. package/skills/playwright-cli-case-generator/SKILL.md +4 -4
  182. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +0 -1
  183. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +0 -1
  184. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +0 -1
  185. package/dist/worker/console/static/assets/index-B28Onmfy.css +0 -1
  186. package/dist/worker/console/static/assets/index-CTMZsDmh.js +0 -486
  187. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +0 -1
  188. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +0 -8
@@ -0,0 +1,89 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { mkdir, open, realpath, rename, rm, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { z } from "zod";
6
+ const stateSchema = z.object({
7
+ cookies: z.array(z.object({
8
+ name: z.string().min(1), value: z.string(), domain: z.string().min(1),
9
+ path: z.string().startsWith("/"), expires: z.number().finite(),
10
+ httpOnly: z.boolean(), secure: z.boolean(), sameSite: z.enum(["Strict", "Lax", "None"]),
11
+ partitionKey: z.string().optional(),
12
+ }).strict()).max(3000),
13
+ origins: z.array(z.object({
14
+ origin: z.string().refine((value) => {
15
+ try {
16
+ const url = new URL(value);
17
+ return ["http:", "https:"].includes(url.protocol) && url.origin === value;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }),
23
+ localStorage: z.array(z.object({ name: z.string(), value: z.string() }).strict()).max(10000),
24
+ }).strict()).max(100),
25
+ }).strict();
26
+ export async function exportBrowserStorage(context, directory) {
27
+ await mkdir(directory, { recursive: true, mode: 0o700 });
28
+ const state = await context.storageState();
29
+ const target = path.join(directory, `${randomUUID()}.json`);
30
+ const temporary = `${target}.tmp`;
31
+ try {
32
+ await writeFile(temporary, JSON.stringify(state), { mode: 0o600, flag: "wx" });
33
+ await rename(temporary, target);
34
+ }
35
+ finally {
36
+ await rm(temporary, { force: true });
37
+ }
38
+ return { path: target, cookieCount: state.cookies.length, origins: state.origins.map((entry) => entry.origin), scope: "cookies+localStorage", excluded: ["sessionStorage", "IndexedDB", "OPFS", "credentials"] };
39
+ }
40
+ /** Parse and validate fully before obtaining or mutating a browser context. */
41
+ export async function readBrowserStorage(filename, directory) {
42
+ const root = await realpath(directory);
43
+ const parent = await realpath(path.dirname(path.resolve(filename)));
44
+ const target = path.join(parent, path.basename(filename));
45
+ if (parent !== root || path.extname(target) !== ".json")
46
+ throw new Error("storage import requires a JSON file directly in the browser storage directory");
47
+ const handle = await open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
48
+ try {
49
+ const stat = await handle.stat();
50
+ if (!stat.isFile() || stat.size > 5 * 1024 * 1024)
51
+ throw new Error("storage file must be regular and at most 5 MiB");
52
+ const raw = await handle.readFile("utf8");
53
+ try {
54
+ return stateSchema.parse(JSON.parse(raw));
55
+ }
56
+ catch {
57
+ throw new Error("invalid storage state: only cookies and http(s) origin localStorage are supported");
58
+ }
59
+ }
60
+ finally {
61
+ await handle.close();
62
+ }
63
+ }
64
+ export async function mergeBrowserStorage(context, state) {
65
+ // A temporary routed document writes only listed keys, without running the site's application.
66
+ const page = await context.newPage();
67
+ let cdp;
68
+ try {
69
+ cdp = await context.newCDPSession(page);
70
+ // page.route alone does not intercept a response supplied by an installed service worker.
71
+ await cdp.send("Network.enable");
72
+ await cdp.send("Network.setBypassServiceWorker", { bypass: true });
73
+ await page.route("**/*", (route) => route.fulfill({ status: 200, contentType: "text/html", body: "<!doctype html><title>Import login state</title>" }));
74
+ await context.addCookies(state.cookies);
75
+ for (const entry of state.origins) {
76
+ await page.goto(entry.origin, { waitUntil: "domcontentloaded", timeout: 10_000 });
77
+ await page.evaluate((items) => { for (const item of items)
78
+ localStorage.setItem(item.name, item.value); }, entry.localStorage);
79
+ }
80
+ }
81
+ catch (cause) {
82
+ throw new Error("storage merge failed; some entries may have been applied, inspect state before retrying", { cause });
83
+ }
84
+ finally {
85
+ await cdp?.detach().catch(() => undefined);
86
+ await page.close();
87
+ }
88
+ return { mode: "merge", cookieCount: state.cookies.length, origins: state.origins.map((entry) => entry.origin), reloadRequired: true };
89
+ }
@@ -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
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Pet persistence — ported from @linxin666/dsh-pet persist.ts, retargeted
3
+ * from $DSH_HOME to the console data directory. One JSON file, atomic
4
+ * rename write, tolerant read (corrupt file falls back to defaults).
5
+ * @module loop-agent/console/chat/pet/pet-persist
6
+ */
7
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { PET_AFFINITY_MAX, emptyPetAffinity, } from "./pet-affinity.js";
10
+ import { defaultPetTreatConfig, emptyPetTreatLedger, } from "./pet-affinity.js";
11
+ export const DEFAULT_PET_ID = "ember";
12
+ export const DEFAULT_PET_NAME = "小橘灵";
13
+ export const defaultPetDisplayConfig = {
14
+ visible: false,
15
+ size: 160,
16
+ right: 24,
17
+ bottom: 120,
18
+ };
19
+ export const PET_DISPLAY_SIZE_MIN = 32;
20
+ export const PET_DISPLAY_SIZE_MAX = 1024;
21
+ export const PET_DISPLAY_INSET_MAX = 10_000;
22
+ export const PET_NAME_MAX_LENGTH = 20;
23
+ export function emptyPetPersist() {
24
+ return {
25
+ petPersistVersion: 2,
26
+ petId: DEFAULT_PET_ID,
27
+ names: {},
28
+ affinity: emptyPetAffinity(),
29
+ treats: emptyPetTreatLedger(),
30
+ display: { ...defaultPetDisplayConfig },
31
+ };
32
+ }
33
+ function finiteNum(value, fallback) {
34
+ return typeof value === "number" && Number.isFinite(value)
35
+ ? value
36
+ : fallback;
37
+ }
38
+ function clamp(value, max) {
39
+ return Math.min(max, Math.max(0, value));
40
+ }
41
+ function loadPetNames(parsed) {
42
+ const names = {};
43
+ if (typeof parsed.names !== "object" || parsed.names === null)
44
+ return names;
45
+ for (const [id, value] of Object.entries(parsed.names)) {
46
+ if (id === "" || typeof value !== "string")
47
+ continue;
48
+ const name = value.trim();
49
+ if (name === "")
50
+ continue;
51
+ names[id] = name.slice(0, PET_NAME_MAX_LENGTH);
52
+ }
53
+ return names;
54
+ }
55
+ /** Load persisted state; missing or corrupt files fall back to defaults. */
56
+ export function loadPetPersist(dir) {
57
+ try {
58
+ const raw = readFileSync(join(dir, "pet.json"), "utf8");
59
+ const parsed = JSON.parse(raw);
60
+ const base = emptyPetPersist();
61
+ const rawAffinity = (parsed.affinity ?? {});
62
+ const affinity = {
63
+ points: clamp(finiteNum(rawAffinity.points, 0), PET_AFFINITY_MAX),
64
+ lastPetAt: clamp(finiteNum(rawAffinity.lastPetAt, 0), Number.MAX_SAFE_INTEGER),
65
+ lastFeedAt: clamp(finiteNum(rawAffinity.lastFeedAt, 0), Number.MAX_SAFE_INTEGER),
66
+ pets: clamp(finiteNum(rawAffinity.pets, 0), Number.MAX_SAFE_INTEGER),
67
+ feeds: clamp(finiteNum(rawAffinity.feeds, 0), Number.MAX_SAFE_INTEGER),
68
+ petRejects: clamp(finiteNum(rawAffinity.petRejects, 0), Number.MAX_SAFE_INTEGER),
69
+ feedRejects: clamp(finiteNum(rawAffinity.feedRejects, 0), Number.MAX_SAFE_INTEGER),
70
+ turns: clamp(finiteNum(rawAffinity.turns, 0), Number.MAX_SAFE_INTEGER),
71
+ };
72
+ const rawTreats = (parsed.treats ?? {});
73
+ const treats = {
74
+ treats: clamp(finiteNum(rawTreats.treats, 0), defaultPetTreatConfig.maxTreats),
75
+ lastTreatGrantAt: clamp(finiteNum(rawTreats.lastTreatGrantAt, 0), Number.MAX_SAFE_INTEGER),
76
+ turnsAtLastTreatGrant: clamp(finiteNum(rawTreats.turnsAtLastTreatGrant, 0), Number.MAX_SAFE_INTEGER),
77
+ };
78
+ const rawDisplay = (parsed.display ?? {});
79
+ const display = {
80
+ visible: parsed.petPersistVersion === 2 && typeof rawDisplay.visible === "boolean"
81
+ ? rawDisplay.visible
82
+ : base.display.visible,
83
+ size: Math.round(Math.min(PET_DISPLAY_SIZE_MAX, Math.max(PET_DISPLAY_SIZE_MIN, finiteNum(rawDisplay.size, base.display.size)))),
84
+ right: Math.round(clamp(finiteNum(rawDisplay.right, base.display.right), PET_DISPLAY_INSET_MAX)),
85
+ bottom: Math.round(clamp(finiteNum(rawDisplay.bottom, base.display.bottom), PET_DISPLAY_INSET_MAX)),
86
+ };
87
+ const petId = typeof parsed.petId === "string" && parsed.petId.trim() !== ""
88
+ ? parsed.petId.trim()
89
+ : base.petId;
90
+ const names = loadPetNames(parsed);
91
+ if (typeof parsed.name === "string" &&
92
+ parsed.name.trim() !== "" &&
93
+ names[petId] === undefined) {
94
+ names[petId] = parsed.name.trim().slice(0, PET_NAME_MAX_LENGTH);
95
+ }
96
+ return { petPersistVersion: 2, petId, names, affinity, treats, display };
97
+ }
98
+ catch {
99
+ return emptyPetPersist();
100
+ }
101
+ }
102
+ /** Atomically persist state (write temp + rename). */
103
+ export function savePetPersist(data, dir) {
104
+ mkdirSync(dir, { recursive: true });
105
+ const target = join(dir, "pet.json");
106
+ const tmp = `${target}.tmp`;
107
+ writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
108
+ renameSync(tmp, target);
109
+ }