minimal-agent 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (573) hide show
  1. package/README.md +11 -3
  2. package/package.json +52 -5
  3. package/plugins/computer_use/.claude-plugin/plugin.json +6 -0
  4. package/plugins/computer_use/README.md +94 -0
  5. package/plugins/computer_use/app/_template.md +142 -0
  6. package/plugins/computer_use/app/chrome.md +165 -0
  7. package/plugins/computer_use/app/finder.md +76 -0
  8. package/plugins/computer_use/app/macos-system-settings.md +81 -0
  9. package/plugins/computer_use/app/microsoft-excel.md +80 -0
  10. package/plugins/computer_use/app/microsoft-powerpoint.md +80 -0
  11. package/plugins/computer_use/app/microsoft-word.md +79 -0
  12. package/plugins/computer_use/app/wechat.md +413 -0
  13. package/plugins/computer_use/app/weread.md +319 -0
  14. package/plugins/computer_use/commands/computeruse.md +25 -0
  15. package/plugins/computer_use/commands/cu.md +9 -0
  16. package/plugins/computer_use/plugin.js +26 -0
  17. package/plugins/computer_use/plugin.ts +30 -0
  18. package/plugins/computer_use/src/capture.js +90 -0
  19. package/plugins/computer_use/src/capture.ts +104 -0
  20. package/plugins/computer_use/src/commands.js +91 -0
  21. package/plugins/computer_use/src/commands.ts +103 -0
  22. package/plugins/computer_use/src/daemon.js +134 -0
  23. package/plugins/computer_use/src/daemon.ts +161 -0
  24. package/plugins/computer_use/src/manuals.js +135 -0
  25. package/plugins/computer_use/src/manuals.ts +159 -0
  26. package/plugins/computer_use/src/verify.js +168 -0
  27. package/plugins/computer_use/src/verify.ts +219 -0
  28. package/plugins/computer_use/test/commands.test.ts +73 -0
  29. package/plugins/cron-runner/plugin.ts +38 -0
  30. package/plugins/cron-runner/src/commands.ts +373 -0
  31. package/plugins/cron-runner/src/cronTime.ts +155 -0
  32. package/plugins/cron-runner/src/lock.ts +79 -0
  33. package/plugins/cron-runner/src/runs.ts +99 -0
  34. package/plugins/cron-runner/src/scheduleEngine.ts +76 -0
  35. package/plugins/cron-runner/src/scheduler.ts +376 -0
  36. package/plugins/cron-runner/src/schema.ts +139 -0
  37. package/plugins/cron-runner/src/service.ts +193 -0
  38. package/plugins/cron-runner/src/store.ts +333 -0
  39. package/plugins/cron-runner/src/types.ts +141 -0
  40. package/plugins/cron-runner/test/cronTime.test.ts +41 -0
  41. package/plugins/cron-runner/test/plugin-integration.test.ts +47 -0
  42. package/plugins/cron-runner/test/scheduleEngine.test.ts +41 -0
  43. package/plugins/cron-runner/test/service.test.ts +76 -0
  44. package/plugins/cron-runner/test/store-and-scheduler.test.ts +501 -0
  45. package/plugins/workflow-runner/plugin.js +56 -2
  46. package/plugins/workflow-runner/plugin.ts +113 -0
  47. package/plugins/workflow-runner/src/agentCommon.js +43 -4
  48. package/plugins/workflow-runner/src/agentCommon.ts +435 -0
  49. package/plugins/workflow-runner/src/controlFlow/branch.js +40 -0
  50. package/plugins/workflow-runner/src/controlFlow/branch.ts +46 -0
  51. package/plugins/workflow-runner/src/controlFlow/loop.js +211 -0
  52. package/plugins/workflow-runner/src/controlFlow/loop.ts +248 -0
  53. package/plugins/workflow-runner/src/controlFlow/pause.js +129 -0
  54. package/plugins/workflow-runner/src/controlFlow/pause.ts +141 -0
  55. package/plugins/workflow-runner/src/controlFlow/signal.js +21 -0
  56. package/plugins/workflow-runner/src/controlFlow/signal.ts +29 -0
  57. package/plugins/workflow-runner/src/controlFlow/types.js +18 -0
  58. package/plugins/workflow-runner/src/controlFlow/types.ts +63 -0
  59. package/plugins/workflow-runner/src/dslManifest.ts +127 -0
  60. package/plugins/workflow-runner/src/expressions.ts +622 -0
  61. package/plugins/workflow-runner/src/index.js +13 -4
  62. package/plugins/workflow-runner/src/index.ts +338 -0
  63. package/plugins/workflow-runner/src/jsonPayload.ts +84 -0
  64. package/plugins/workflow-runner/src/loader.js +23 -0
  65. package/plugins/workflow-runner/src/loader.ts +721 -0
  66. package/plugins/workflow-runner/src/multimodal.js +2 -1
  67. package/plugins/workflow-runner/src/multimodal.ts +64 -0
  68. package/plugins/workflow-runner/src/recovery.ts +149 -0
  69. package/plugins/workflow-runner/src/runner.js +75 -659
  70. package/plugins/workflow-runner/src/runner.ts +604 -0
  71. package/plugins/workflow-runner/src/stepExecutors/agent.ts +234 -0
  72. package/plugins/workflow-runner/src/stepExecutors/assert.ts +50 -0
  73. package/plugins/workflow-runner/src/stepExecutors/echo.js +31 -0
  74. package/plugins/workflow-runner/src/stepExecutors/echo.ts +38 -0
  75. package/plugins/workflow-runner/src/stepExecutors/llm.ts +133 -0
  76. package/plugins/workflow-runner/src/stepExecutors/set.js +49 -0
  77. package/plugins/workflow-runner/src/stepExecutors/set.ts +56 -0
  78. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  79. package/plugins/workflow-runner/src/stepExecutors/skill.ts +140 -0
  80. package/plugins/workflow-runner/src/stepExecutors/tool.js +80 -8
  81. package/plugins/workflow-runner/src/stepExecutors/tool.ts +256 -0
  82. package/plugins/workflow-runner/src/stepExecutors/verify.ts +68 -0
  83. package/plugins/workflow-runner/src/stepPipeline.js +277 -0
  84. package/plugins/workflow-runner/src/stepPipeline.ts +366 -0
  85. package/plugins/workflow-runner/src/taskIdentity.ts +405 -0
  86. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  87. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  88. package/plugins/workflow-runner/src/types.js +37 -1
  89. package/plugins/workflow-runner/src/types.ts +602 -0
  90. package/plugins/workflow-runner/src/workflowState.ts +733 -0
  91. package/plugins/workflow-runner/test/agent.test.ts +587 -0
  92. package/plugins/workflow-runner/test/agentLlmError.test.ts +373 -0
  93. package/plugins/workflow-runner/test/bashBlacklist.sweep.test.ts +65 -0
  94. package/plugins/workflow-runner/test/captureMisbind.test.ts +99 -0
  95. package/plugins/workflow-runner/test/cli.e2e.test.ts +147 -0
  96. package/plugins/workflow-runner/test/context-files.test.ts +320 -0
  97. package/plugins/workflow-runner/test/e2e.test.ts +370 -0
  98. package/plugins/workflow-runner/test/engineRobustness.test.ts +226 -0
  99. package/plugins/workflow-runner/test/expressions.test.ts +397 -0
  100. package/plugins/workflow-runner/test/foundation.test.ts +126 -0
  101. package/plugins/workflow-runner/test/graceful.test.ts +139 -0
  102. package/plugins/workflow-runner/test/greedyInputs.load.test.ts +47 -0
  103. package/plugins/workflow-runner/test/hostCapability.test.ts +108 -0
  104. package/plugins/workflow-runner/test/illustrate.load.test.ts +183 -0
  105. package/plugins/workflow-runner/test/jsonPayload.test.ts +83 -0
  106. package/plugins/workflow-runner/test/loader.test.ts +617 -0
  107. package/plugins/workflow-runner/test/maxTurnsRetryDelay.test.ts +33 -0
  108. package/plugins/workflow-runner/test/multimodal.test.ts +342 -0
  109. package/plugins/workflow-runner/test/output-schema.test.ts +213 -0
  110. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +87 -0
  111. package/plugins/workflow-runner/test/pluginRunner.isolation.test.ts +269 -0
  112. package/plugins/workflow-runner/test/ppt.load.test.ts +1220 -0
  113. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +220 -0
  114. package/plugins/workflow-runner/test/recovery.test.ts +265 -0
  115. package/plugins/workflow-runner/test/runner.test.ts +911 -0
  116. package/plugins/workflow-runner/test/runnerPreflight.test.ts +379 -0
  117. package/plugins/workflow-runner/test/skillTurnDone.test.ts +71 -0
  118. package/plugins/workflow-runner/test/stateLifecycle.test.ts +150 -0
  119. package/plugins/workflow-runner/test/stepPipeline.test.ts +114 -0
  120. package/plugins/workflow-runner/test/taskArbitration.test.ts +338 -0
  121. package/plugins/workflow-runner/test/taskIdentity.test.ts +237 -0
  122. package/plugins/workflow-runner/test/taskLifecycle.test.ts +270 -0
  123. package/plugins/workflow-runner/test/toolArtifactOverwrite.test.ts +221 -0
  124. package/plugins/workflow-runner/test/toolCapabilityBoundary.test.ts +29 -0
  125. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  126. package/plugins/workflow-runner/test/webppt.load.test.ts +250 -0
  127. package/plugins/workflow-runner/test/word.load.test.ts +208 -0
  128. package/plugins/workflow-runner/test/workflows.smoke.test.ts +76 -0
  129. package/plugins/workflow-runner/test/yamlGuardConditions.test.ts +207 -0
  130. package/skills/browser-automation/SKILL.md +9 -19
  131. package/skills/web-crawler/SKILL.md +9 -11
  132. package/src/askBridge.ts +249 -0
  133. package/src/bootstrap/cwdArg.js +10 -3
  134. package/src/bootstrap/cwdArg.ts +31 -0
  135. package/src/bootstrap/workingDir.ts +48 -0
  136. package/src/cli/args.js +70 -7
  137. package/src/cli/args.ts +146 -0
  138. package/src/cli/computerUse.js +185 -0
  139. package/src/cli/computerUse.ts +221 -0
  140. package/src/cli/configMenu.js +274 -202
  141. package/src/cli/configMenu.tsx +521 -0
  142. package/src/cli/memoryAccess.js +45 -0
  143. package/src/cli/memoryAccess.ts +53 -0
  144. package/src/cli/print.js +119 -5
  145. package/src/cli/print.ts +597 -0
  146. package/src/cli/quickModelSetup.js +60 -0
  147. package/src/cli/quickModelSetup.tsx +96 -0
  148. package/src/cli/streamJson.js +80 -4
  149. package/src/cli/streamJson.ts +433 -0
  150. package/src/cli/wizards/advisorWizard.js +1 -0
  151. package/src/cli/wizards/advisorWizard.tsx +195 -0
  152. package/src/cli/wizards/agentRuntimeWizard.js +84 -0
  153. package/src/cli/wizards/agentRuntimeWizard.tsx +134 -0
  154. package/src/cli/wizards/browserWizard.js +55 -0
  155. package/src/cli/wizards/browserWizard.tsx +77 -0
  156. package/src/cli/wizards/contextAdvisorWizard.js +84 -0
  157. package/src/cli/wizards/contextAdvisorWizard.tsx +124 -0
  158. package/src/cli/wizards/fieldForm.js +7 -4
  159. package/src/cli/wizards/fieldForm.tsx +250 -0
  160. package/src/cli/wizards/imageWizard.js +4 -5
  161. package/src/cli/wizards/imageWizard.tsx +96 -0
  162. package/src/cli/wizards/openRouterWizard.tsx +48 -0
  163. package/src/cli/wizards/providerPresets.js +1 -8
  164. package/src/cli/wizards/providerPresets.ts +106 -0
  165. package/src/cli/wizards/providerWizard.js +152 -205
  166. package/src/cli/wizards/providerWizard.tsx +266 -0
  167. package/src/cli/wizards/searchWizard.tsx +47 -0
  168. package/src/cli/wizards/ttsWizard.js +4 -3
  169. package/src/cli/wizards/ttsWizard.tsx +61 -0
  170. package/src/config/configDefaults.js +25 -0
  171. package/src/config/configDefaults.ts +33 -0
  172. package/src/config/configDiagnostics.js +117 -0
  173. package/src/config/configDiagnostics.ts +166 -0
  174. package/src/config/configFile.js +52 -32
  175. package/src/config/configFile.ts +633 -0
  176. package/src/config/configManagement.js +126 -0
  177. package/src/config/configManagement.ts +159 -0
  178. package/src/config/configService.js +66 -11
  179. package/src/config/configService.ts +554 -0
  180. package/src/config/numericLimits.js +61 -0
  181. package/src/config/numericLimits.ts +80 -0
  182. package/src/config.ts +109 -0
  183. package/src/context/agentHome.ts +11 -0
  184. package/src/context/archive.ts +144 -0
  185. package/src/context/budgetPlan.js +54 -0
  186. package/src/context/budgetPlan.ts +88 -0
  187. package/src/context/compact.js +179 -106
  188. package/src/context/compact.ts +584 -0
  189. package/src/context/contextBundle.js +119 -0
  190. package/src/context/contextBundle.ts +176 -0
  191. package/src/context/contextLock.ts +84 -0
  192. package/src/context/microCompactLite.ts +218 -0
  193. package/src/context/persistContext.ts +177 -0
  194. package/src/context/reactiveCompact.js +19 -4
  195. package/src/context/reactiveCompact.ts +240 -0
  196. package/src/context/recentDirs.ts +42 -0
  197. package/src/context/sessionPath.ts +206 -0
  198. package/src/context/sessionRegistry.js +8 -2
  199. package/src/context/sessionRegistry.ts +414 -0
  200. package/src/context/snipCompact.ts +163 -0
  201. package/src/context/tokenCounter.ts +106 -0
  202. package/src/context/workspaceIndex.js +2 -2
  203. package/src/context/workspaceIndex.ts +751 -0
  204. package/src/entry.compiled.tsx +20 -0
  205. package/src/harness/claimValidation.js +9 -0
  206. package/src/harness/claimValidation.ts +22 -0
  207. package/src/harness/evidenceLedger.js +92 -0
  208. package/src/harness/evidenceLedger.ts +96 -0
  209. package/src/harness/stallGuard.js +60 -0
  210. package/src/harness/stallGuard.ts +81 -0
  211. package/src/humanIntervention.js +129 -0
  212. package/src/humanIntervention.ts +216 -0
  213. package/src/infra/verification.ts +295 -0
  214. package/src/irreversible/rules.js +216 -0
  215. package/src/irreversible/rules.ts +261 -0
  216. package/src/llm/client.js +40 -3
  217. package/src/llm/client.ts +621 -0
  218. package/src/llm/promptCache.js +27 -0
  219. package/src/llm/promptCache.ts +41 -0
  220. package/src/llm/providers/anthropicFormat.js +41 -5
  221. package/src/llm/providers/anthropicFormat.ts +290 -0
  222. package/src/llm/providers/openaiFormat.js +186 -8
  223. package/src/llm/providers/openaiFormat.ts +428 -0
  224. package/src/loop.js +614 -63
  225. package/src/loop.ts +1454 -0
  226. package/src/main.js +67 -10
  227. package/src/main.tsx +344 -0
  228. package/src/memory/codec.js +204 -0
  229. package/src/memory/codec.ts +249 -0
  230. package/src/memory/constants.js +34 -0
  231. package/src/memory/constants.ts +50 -0
  232. package/src/memory/episodicMemory.js +684 -0
  233. package/src/memory/episodicMemory.ts +815 -0
  234. package/src/memory/fileStore.js +148 -0
  235. package/src/memory/fileStore.ts +184 -0
  236. package/src/memory/memoryService.js +570 -0
  237. package/src/memory/memoryService.ts +859 -0
  238. package/src/memory/migration.js +128 -0
  239. package/src/memory/migration.ts +160 -0
  240. package/src/memory/repository.js +789 -0
  241. package/src/memory/repository.ts +893 -0
  242. package/src/memory/retrieval.js +108 -0
  243. package/src/memory/retrieval.ts +145 -0
  244. package/src/memory/searchIndex.js +107 -0
  245. package/src/memory/searchIndex.ts +153 -0
  246. package/src/memory/topicView.js +82 -0
  247. package/src/memory/topicView.ts +95 -0
  248. package/src/memory/turnJournal.js +95 -0
  249. package/src/memory/turnJournal.ts +120 -0
  250. package/src/memory/types.js +2 -0
  251. package/src/memory/types.ts +186 -0
  252. package/src/messageQueue.ts +123 -0
  253. package/src/multimodal.js +128 -46
  254. package/src/multimodal.ts +349 -0
  255. package/src/plugin-sdk.js +21 -1
  256. package/src/plugin-sdk.ts +134 -0
  257. package/src/plugins/builtinPluginIds.js +47 -0
  258. package/src/plugins/builtinPluginIds.ts +51 -0
  259. package/src/plugins/builtinRegistry.ts +43 -0
  260. package/src/plugins/commandRouter.js +40 -26
  261. package/src/plugins/commandRouter.ts +261 -0
  262. package/src/plugins/hookEngine.ts +341 -0
  263. package/src/plugins/pluginApi.ts +133 -0
  264. package/src/plugins/pluginLifecycle.ts +34 -0
  265. package/src/plugins/pluginLoader.ts +147 -0
  266. package/src/plugins/pluginManifest.js +127 -0
  267. package/src/plugins/pluginManifest.ts +152 -0
  268. package/src/plugins/pluginRunner.js +47 -2
  269. package/src/plugins/pluginRunner.ts +207 -0
  270. package/src/plugins/transcript.ts +208 -0
  271. package/src/prompts/lint.js +169 -0
  272. package/src/prompts/lint.ts +220 -0
  273. package/src/prompts/pluginList.js +32 -16
  274. package/src/prompts/pluginList.ts +86 -0
  275. package/src/prompts/policyRegistry.js +318 -0
  276. package/src/prompts/policyRegistry.ts +358 -0
  277. package/src/prompts/projectInstructions.js +24 -0
  278. package/src/prompts/projectInstructions.ts +81 -0
  279. package/src/prompts/promptAssembly.js +311 -202
  280. package/src/prompts/promptAssembly.ts +510 -0
  281. package/src/prompts/skillList.js +106 -45
  282. package/src/prompts/skillList.ts +198 -0
  283. package/src/prompts/system.js +31 -6
  284. package/src/prompts/system.ts +81 -0
  285. package/src/runtime/agentRuntimeContext.js +53 -0
  286. package/src/runtime/agentRuntimeContext.ts +112 -0
  287. package/src/session/runAgentTurn.js +71 -12
  288. package/src/session/runAgentTurn.ts +181 -0
  289. package/src/session/runTurn.js +29 -0
  290. package/src/session/runTurn.ts +160 -0
  291. package/src/session/runtimeStore.js +149 -0
  292. package/src/session/runtimeStore.ts +197 -0
  293. package/src/session/sessionState.js +79 -3
  294. package/src/session/sessionState.ts +134 -0
  295. package/src/toolGate.ts +50 -0
  296. package/src/tools/advisor/advisor.js +76 -15
  297. package/src/tools/advisor/advisor.ts +512 -0
  298. package/src/tools/agent/agent.js +245 -29
  299. package/src/tools/agent/agent.ts +837 -0
  300. package/src/tools/agent/agentTaskService.js +60 -0
  301. package/src/tools/agent/agentTaskService.ts +113 -0
  302. package/src/tools/agent/agentTypes.js +20 -8
  303. package/src/tools/agent/agentTypes.ts +94 -0
  304. package/src/tools/agent/coordinator.ts +111 -0
  305. package/src/tools/agent/worktree.ts +296 -0
  306. package/src/tools/ask/ask.js +17 -0
  307. package/src/tools/ask/ask.ts +122 -0
  308. package/src/tools/bash/attachImage.js +223 -0
  309. package/src/tools/bash/attachImage.ts +281 -0
  310. package/src/tools/bash/bash.js +132 -14
  311. package/src/tools/bash/bash.ts +642 -0
  312. package/src/tools/bash/semantics.ts +103 -0
  313. package/src/tools/bash/warnings.js +17 -11
  314. package/src/tools/bash/warnings.ts +117 -0
  315. package/src/tools/capabilities.js +42 -0
  316. package/src/tools/capabilities.ts +69 -0
  317. package/src/tools/computeruse/accessibilityPermission.js +261 -0
  318. package/src/tools/computeruse/accessibilityPermission.ts +294 -0
  319. package/src/tools/computeruse/axSearch.js +96 -0
  320. package/src/tools/computeruse/axSearch.ts +120 -0
  321. package/src/tools/computeruse/axSnap.js +272 -0
  322. package/src/tools/computeruse/axSnap.ts +349 -0
  323. package/src/tools/computeruse/capabilityLabels.js +95 -0
  324. package/src/tools/computeruse/capabilityLabels.ts +115 -0
  325. package/src/tools/computeruse/computerUseService.js +2235 -0
  326. package/src/tools/computeruse/computerUseService.ts +2686 -0
  327. package/src/tools/computeruse/computeruse.js +2742 -0
  328. package/src/tools/computeruse/computeruse.ts +3265 -0
  329. package/src/tools/computeruse/diagnosticTrace.js +154 -0
  330. package/src/tools/computeruse/diagnosticTrace.ts +195 -0
  331. package/src/tools/computeruse/diagnostics.js +369 -0
  332. package/src/tools/computeruse/diagnostics.ts +453 -0
  333. package/src/tools/computeruse/grants.js +53 -0
  334. package/src/tools/computeruse/grants.ts +70 -0
  335. package/src/tools/computeruse/humanBoundary.js +159 -0
  336. package/src/tools/computeruse/humanBoundary.ts +199 -0
  337. package/src/tools/computeruse/intervention.js +77 -0
  338. package/src/tools/computeruse/intervention.ts +110 -0
  339. package/src/tools/computeruse/manualSchema.js +709 -0
  340. package/src/tools/computeruse/manualSchema.ts +916 -0
  341. package/src/tools/computeruse/observationImages.js +705 -0
  342. package/src/tools/computeruse/observationImages.ts +889 -0
  343. package/src/tools/computeruse/protocol.js +60 -0
  344. package/src/tools/computeruse/protocol.ts +515 -0
  345. package/src/tools/computeruse/telemetryLog.js +57 -0
  346. package/src/tools/computeruse/telemetryLog.ts +67 -0
  347. package/src/tools/computeruse/vision/appGuide.js +325 -0
  348. package/src/tools/computeruse/vision/appGuide.ts +482 -0
  349. package/src/tools/computeruse/vision/focus.js +291 -0
  350. package/src/tools/computeruse/vision/focus.ts +382 -0
  351. package/src/tools/computeruse/vision/frames.js +248 -0
  352. package/src/tools/computeruse/vision/frames.ts +329 -0
  353. package/src/tools/computeruse/vision/geometry.js +131 -0
  354. package/src/tools/computeruse/vision/geometry.ts +163 -0
  355. package/src/tools/computeruse/vision/imaging.js +201 -0
  356. package/src/tools/computeruse/vision/imaging.ts +281 -0
  357. package/src/tools/computeruse/vision/png.js +274 -0
  358. package/src/tools/computeruse/vision/png.ts +317 -0
  359. package/src/tools/computeruse/vision/raster.js +320 -0
  360. package/src/tools/computeruse/vision/raster.ts +408 -0
  361. package/src/tools/computeruse/vision/settle.js +131 -0
  362. package/src/tools/computeruse/vision/settle.ts +220 -0
  363. package/src/tools/computeruse/vision/telemetry.js +196 -0
  364. package/src/tools/computeruse/vision/telemetry.ts +241 -0
  365. package/src/tools/computeruse/visionOcr.js +115 -0
  366. package/src/tools/computeruse/visionOcr.ts +128 -0
  367. package/src/tools/computeruse/waitFor.js +237 -0
  368. package/src/tools/computeruse/waitFor.ts +291 -0
  369. package/src/tools/download/download.js +10 -6
  370. package/src/tools/download/download.ts +187 -0
  371. package/src/tools/edit/edit.js +30 -18
  372. package/src/tools/edit/edit.ts +349 -0
  373. package/src/tools/edit/multi-edit.js +25 -14
  374. package/src/tools/edit/multi-edit.ts +223 -0
  375. package/src/tools/effectiveToolSet.js +73 -0
  376. package/src/tools/effectiveToolSet.ts +91 -0
  377. package/src/tools/genimage/genimage.ts +257 -0
  378. package/src/tools/genspeech/genspeech.ts +83 -0
  379. package/src/tools/glob/glob.js +176 -29
  380. package/src/tools/glob/glob.ts +283 -0
  381. package/src/tools/grep/grep.js +61 -19
  382. package/src/tools/grep/grep.ts +239 -0
  383. package/src/tools/grep/rgPath.ts +206 -0
  384. package/src/tools/index.js +99 -5
  385. package/src/tools/index.ts +293 -0
  386. package/src/tools/lsp/config.js +113 -0
  387. package/src/tools/lsp/config.ts +113 -0
  388. package/src/tools/lsp/lsp.js +256 -0
  389. package/src/tools/lsp/lsp.ts +274 -0
  390. package/src/tools/lsp/service.js +303 -0
  391. package/src/tools/lsp/service.ts +296 -0
  392. package/src/tools/memory/memory.js +520 -0
  393. package/src/tools/memory/memory.ts +625 -0
  394. package/src/tools/memory/policy.js +53 -0
  395. package/src/tools/memory/policy.ts +72 -0
  396. package/src/tools/notebook/notebookEdit.js +114 -0
  397. package/src/tools/notebook/notebookEdit.ts +126 -0
  398. package/src/tools/powershell/powershell.js +210 -0
  399. package/src/tools/powershell/powershell.ts +239 -0
  400. package/src/tools/presentresult/presentresult.js +145 -0
  401. package/src/tools/presentresult/presentresult.ts +156 -0
  402. package/src/tools/read/read.js +371 -163
  403. package/src/tools/read/read.ts +558 -0
  404. package/src/tools/shared/diffSummary.js +44 -0
  405. package/src/tools/shared/diffSummary.ts +63 -0
  406. package/src/tools/shared/fileActivity.js +51 -0
  407. package/src/tools/shared/fileActivity.ts +66 -0
  408. package/src/tools/shared/fileMutationBus.js +13 -0
  409. package/src/tools/shared/fileMutationBus.ts +14 -0
  410. package/src/tools/shared/fileState.js +73 -65
  411. package/src/tools/shared/fileState.ts +145 -0
  412. package/src/tools/shared/fileUtils.js +81 -4
  413. package/src/tools/shared/fileUtils.ts +504 -0
  414. package/src/tools/shared/killTree.ts +50 -0
  415. package/src/tools/shared/permanentDeleteGuard.js +26 -0
  416. package/src/tools/shared/permanentDeleteGuard.ts +34 -0
  417. package/src/tools/shared/schemas.ts +20 -0
  418. package/src/tools/shared/toolOutputStore.js +97 -0
  419. package/src/tools/shared/toolOutputStore.ts +110 -0
  420. package/src/tools/skill/skill.js +86 -0
  421. package/src/tools/skill/skill.ts +94 -0
  422. package/src/tools/snip/snip.js +67 -0
  423. package/src/tools/snip/snip.ts +80 -0
  424. package/src/tools/tasks/backgroundTaskService.js +275 -0
  425. package/src/tools/tasks/backgroundTaskService.ts +369 -0
  426. package/src/tools/tasks/processEvidence.js +24 -0
  427. package/src/tools/tasks/processEvidence.ts +34 -0
  428. package/src/tools/tasks/taskManagement.js +163 -0
  429. package/src/tools/tasks/taskManagement.ts +180 -0
  430. package/src/tools/tasks/taskOutput.js +113 -0
  431. package/src/tools/tasks/taskOutput.ts +145 -0
  432. package/src/tools/tasks/taskStop.js +74 -0
  433. package/src/tools/tasks/taskStop.ts +99 -0
  434. package/src/tools/tasks/taskStore.js +46 -0
  435. package/src/tools/tasks/taskStore.ts +77 -0
  436. package/src/tools/terminal/terminalCapture.js +167 -0
  437. package/src/tools/terminal/terminalCapture.ts +179 -0
  438. package/src/tools/trash/trash.js +233 -0
  439. package/src/tools/trash/trash.ts +283 -0
  440. package/src/tools/types.js +1 -1
  441. package/src/tools/types.ts +24 -0
  442. package/src/tools/webbrowser/agentBrowserPath.ts +286 -0
  443. package/src/tools/webbrowser/browser.js +214 -11
  444. package/src/tools/webbrowser/browser.ts +293 -0
  445. package/src/tools/webbrowser/browserControl.js +153 -48
  446. package/src/tools/webbrowser/browserControl.ts +262 -0
  447. package/src/tools/webbrowser/browserDaemon.js +147 -39
  448. package/src/tools/webbrowser/browserDaemon.ts +270 -0
  449. package/src/tools/webbrowser/browserOutcome.js +273 -0
  450. package/src/tools/webbrowser/browserOutcome.ts +306 -0
  451. package/src/tools/webbrowser/browserRunRegistry.js +150 -0
  452. package/src/tools/webbrowser/browserRunRegistry.ts +179 -0
  453. package/src/tools/webbrowser/browserService.js +20 -6
  454. package/src/tools/webbrowser/browserService.ts +56 -0
  455. package/src/tools/webbrowser/browserStream.js +30 -1
  456. package/src/tools/webbrowser/browserStream.ts +216 -0
  457. package/src/tools/webbrowser/browserTabs.js +143 -0
  458. package/src/tools/webbrowser/browserTabs.ts +210 -0
  459. package/src/tools/webbrowser/webbrowser.js +831 -150
  460. package/src/tools/webbrowser/webbrowser.ts +1537 -0
  461. package/src/tools/webfetch/preapproved.ts +274 -0
  462. package/src/tools/webfetch/webfetch.js +24 -23
  463. package/src/tools/webfetch/webfetch.ts +383 -0
  464. package/src/tools/websearch/websearch.js +12 -11
  465. package/src/tools/websearch/websearch.ts +292 -0
  466. package/src/tools/worktree/sessionWorktree.js +128 -0
  467. package/src/tools/worktree/sessionWorktree.ts +158 -0
  468. package/src/tools/write/write.js +27 -44
  469. package/src/tools/write/write.ts +136 -0
  470. package/src/types.ts +960 -0
  471. package/src/ui/App.js +216 -12
  472. package/src/ui/App.tsx +879 -0
  473. package/src/ui/AskQuestion.tsx +300 -0
  474. package/src/ui/InputBox.js +77 -1
  475. package/src/ui/InputBox.tsx +567 -0
  476. package/src/ui/LiveArea.tsx +344 -0
  477. package/src/ui/MessageList.js +27 -6
  478. package/src/ui/MessageList.tsx +559 -0
  479. package/src/ui/ModelPicker.tsx +107 -0
  480. package/src/ui/QueuedMessagesBanner.tsx +51 -0
  481. package/src/ui/Root.js +6 -8
  482. package/src/ui/Root.tsx +100 -0
  483. package/src/ui/SessionPicker.tsx +169 -0
  484. package/src/ui/StatusLine.js +22 -1
  485. package/src/ui/StatusLine.tsx +145 -0
  486. package/src/ui/WorkspacePicker.tsx +84 -0
  487. package/src/ui/advisorCommand.js +54 -0
  488. package/src/ui/advisorCommand.ts +63 -0
  489. package/src/ui/computerCommand.js +19 -0
  490. package/src/ui/computerCommand.ts +35 -0
  491. package/src/ui/figures.ts +45 -0
  492. package/src/ui/hooks/askSlot.ts +112 -0
  493. package/src/ui/hooks/useChat.js +271 -7
  494. package/src/ui/hooks/useChat.ts +1578 -0
  495. package/src/ui/hooks/useInputHistory.ts +235 -0
  496. package/src/ui/hooks/usePasteHandler.ts +220 -0
  497. package/src/ui/hooks/useTerminalSize.ts +41 -0
  498. package/src/ui/hooks/useTextBuffer.ts +108 -0
  499. package/src/ui/hooks/useTokenUsage.ts +44 -0
  500. package/src/ui/liveViewport.ts +189 -0
  501. package/src/ui/markdown.ts +230 -0
  502. package/src/ui/textBuffer.ts +239 -0
  503. package/src/ui/theme.ts +35 -0
  504. package/src/utils/clipboardImage.js +358 -0
  505. package/src/utils/clipboardImage.ts +420 -0
  506. package/src/utils/debugLog.ts +50 -0
  507. package/src/utils/greenRoot.ts +35 -0
  508. package/src/utils/packageRoot.ts +40 -0
  509. package/src/utils/pathContains.js +28 -0
  510. package/src/utils/pathContains.ts +31 -0
  511. package/src/utils/resourcePaths.ts +71 -0
  512. package/src/utils/stripThink.ts +49 -0
  513. package/src/utils/zodToJson.ts +73 -0
  514. package/src/web/browserBridge.js +365 -66
  515. package/src/web/browserBridge.ts +672 -0
  516. package/src/web/protocol.ts +1804 -0
  517. package/vendor/computer-use/arm64-darwin/cu-helperd +0 -0
  518. package/workflows/ppt.yaml +13 -2
  519. package/workflows/wechat-reading-shots.yaml +315 -0
  520. package/workflows/word.yaml +11 -1
  521. package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
  522. package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
  523. package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
  524. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  525. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
  526. package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
  527. package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
  528. package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
  529. package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
  530. package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
  531. package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
  532. package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
  533. package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
  534. package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
  535. package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
  536. package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
  537. package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
  538. package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
  539. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
  540. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
  541. package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
  542. package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
  543. package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
  544. package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
  545. package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
  546. package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
  547. package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
  548. package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
  549. package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
  550. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
  551. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
  552. package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
  553. package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
  554. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
  555. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
  556. package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
  557. package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
  558. package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
  559. package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
  560. package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
  561. package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
  562. package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
  563. package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
  564. package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
  565. package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
  566. package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
  567. package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
  568. package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
  569. package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
  570. package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
  571. package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
  572. /package/skills/ai-image/scripts/{generate_test.mjs → generate.selftest.mjs} +0 -0
  573. /package/skills/ai-image/scripts/{generate_from_plan_test.mjs → generate_from_plan.selftest.mjs} +0 -0
@@ -0,0 +1,306 @@
1
+ /**
2
+ * ============================================================
3
+ * browserOutcome.ts —— 浏览器动作的「成功率」设施
4
+ * ------------------------------------------------------------
5
+ * 三件事,全部是纯函数,因此可以被单测钉死:
6
+ * ① 页面指纹与变化判定(动作到底生效了没有)
7
+ * ② 选择器失败时的候选推荐(把「没找到」变成「最接近的是这几个」)
8
+ * ③ 同一目标连续失败的熔断计数
9
+ *
10
+ * 为什么这些属于「成功率」而不是「锦上添花」:模型的每一次重试都是一个 LLM 往返,
11
+ * 而它重试时**手上没有当前页面**——失败只回了一句 `selector not found`。于是最常见
12
+ * 的失败形状不是「做错了」,是「原地换着选择器盲猜三五轮,然后放弃」。
13
+ * ============================================================
14
+ */
15
+
16
+ /** 会被验证「是否真的改变了页面」的动作。 */
17
+ export const VERIFIED_BROWSER_ACTIONS = new Set([
18
+ 'click',
19
+ 'fill',
20
+ 'type',
21
+ 'select',
22
+ 'press',
23
+ 'drag',
24
+ ]);
25
+
26
+ export type BrowserEffect = 'changed' | 'unchanged' | 'unknown';
27
+
28
+ /**
29
+ * 两份快照中内容不同的行号。行数不同则返回 null —— 那本身就是结构变化。
30
+ *
31
+ * ⚠️ 两边都先 `trim()`。动作前的快照是原始 stdout、动作后那份在别处被 trim 过,
32
+ * 一个尾随换行就让行数对不上、直接判成「结构变了」,于是**所有**动作都判 changed,
33
+ * 噪声扣除彻底失效。基准集跑出来才发现——纯逻辑单测里两边都是我手写的字符串,
34
+ * 永远不会有这种不对称。
35
+ */
36
+ export function differingLineIndexes(a: string, b: string): Set<number> | null {
37
+ const left = a.trim().split('\n');
38
+ const right = b.trim().split('\n');
39
+ if (left.length !== right.length) return null;
40
+ const diff = new Set<number>();
41
+ for (let i = 0; i < left.length; i += 1) {
42
+ if (left[i].trim() !== right[i].trim()) diff.add(i);
43
+ }
44
+ return diff;
45
+ }
46
+
47
+ /**
48
+ * 一次生效判定的结论。
49
+ *
50
+ * `maskedLines` 不是调试信息,是**结论的可信度**:它是被当作噪声豁免掉的行数。
51
+ * 判定不可能区分「这一行在自己跳」和「这一行既在自己跳、又刚好被我的动作改了」——
52
+ * 信息论上就是不可分的。所以豁免多少必须随结论一起报出去,由模型自己权衡。
53
+ */
54
+ export interface BrowserEffectVerdict {
55
+ effect: BrowserEffect;
56
+ /** 被噪声基线豁免掉的行数(仅 `unchanged` 时可能 > 0)。 */
57
+ maskedLines: number;
58
+ }
59
+
60
+ /**
61
+ * 判定一次动作是否真的改变了页面。
62
+ *
63
+ * **噪声是测出来的,不是猜出来的**:`after` 之后隔一小段再拍一张 `noiseAfter`,
64
+ * 两张之间自己变了的**行位置**就是噪声源(时钟、未读数、轮询列表)。再看
65
+ * 「动作前→动作后」变了的行里,除掉这些位置之后还剩不剩。
66
+ *
67
+ * ⚠️ 噪声基线取在**动作之后**而不是之前。噪声是页面的持续属性,前后测等价;
68
+ * 而放在后面换来两件事:`before === after` 时(正是要判 unchanged 的情形)根本
69
+ * 不必采样,零成本;采样的那段等待也不再推迟动作本身的发出。
70
+ *
71
+ * 三个实测踩过的坑,这里同时避开:
72
+ * · 直接比全页文本 → 时钟每 200ms 一跳,恒判「变了」,判定形同虚设;
73
+ * · 忽略所有文本只看结构 → 点击把 `StaticText "closed"` 变成 `"opened"` 这种
74
+ * **真正生效**的纯文本变化被报成「完全没有变化」。误报比不报更糟:它在教模型
75
+ * 不要相信一个正确的动作;
76
+ * · 按行**内容**标定噪声 → 时钟每次都是新值,永远匹配不上自己。所以按行**位置**。
77
+ *
78
+ * 行数一变就直接算 changed —— 结构动了,不必再细究。
79
+ */
80
+ export function classifyPageEffect(
81
+ before: string | undefined,
82
+ after: string | undefined,
83
+ noiseAfter?: string,
84
+ ): BrowserEffectVerdict {
85
+ if (before === undefined || after === undefined) return { effect: 'unknown', maskedLines: 0 };
86
+ const actual = differingLineIndexes(before, after);
87
+ if (actual === null) return { effect: 'changed', maskedLines: 0 }; // 行数变了 = 结构变了
88
+ if (actual.size === 0) return { effect: 'unchanged', maskedLines: 0 };
89
+ const noisy = noiseAfter !== undefined ? differingLineIndexes(after, noiseAfter) : null;
90
+ // 没有可用的噪声基线时不做扣除:宁可把「生效了」判宽,也不能把一次真实变化
91
+ // 误报成没变——那会让模型放弃一条本来走通了的路。
92
+ if (noisy === null) return { effect: 'changed', maskedLines: 0 };
93
+ for (const index of actual) if (!noisy.has(index)) return { effect: 'changed', maskedLines: 0 };
94
+ return { effect: 'unchanged', maskedLines: actual.size };
95
+ }
96
+
97
+ /** 给模型的效果说明。措辞要让它知道**下一步该干什么**,不只是知道结果。 */
98
+ export function describeEffect(verdict: BrowserEffectVerdict, action: string): string | null {
99
+ switch (verdict.effect) {
100
+ case 'changed':
101
+ return null; // 正常路径不加噪声
102
+ case 'unchanged':
103
+ return (
104
+ `⚠️ 动作已发出,但页面结构在动作前后**完全没有变化**。\n`
105
+ + `这通常意味着三种情况之一:${action} 命中的不是可交互元素、点击被遮挡层拦住了、`
106
+ + `或者这个操作本来就不改变页面(比如点了一个纯展示文本)。\n`
107
+ + `请对照下面的最新快照确认目标是否正确,不要用同一个选择器直接重试。`
108
+ // 豁免了行才提。这不是免责声明,是让模型知道这个结论有多硬:
109
+ // 被豁免的位置上,「自己在跳」和「被我改了又在跳」不可区分。
110
+ + (verdict.maskedLines > 0
111
+ ? `\n(说明:页面上有 ${verdict.maskedLines} 行在持续自行变化(时钟/轮询一类),`
112
+ + `已从判定中排除。若你这次操作本该影响的正是这些位置,这个结论可能不准。)`
113
+ : '')
114
+ );
115
+ case 'unknown':
116
+ return '(未能取得动作前后的页面快照,无法判断这次操作是否生效——请自行核对。)';
117
+ }
118
+ }
119
+
120
+ /** 从 `@eN` 快照里抽出的一个可点目标。 */
121
+ export interface SnapshotCandidate {
122
+ ref: string;
123
+ line: string;
124
+ }
125
+
126
+ /**
127
+ * 解析快照里的元素引用行。
128
+ *
129
+ * 匹配 `ref=eN` 而不是 `[ref=eN]`:真实行是 `[level=1, ref=e1]`,ref 常和别的属性
130
+ * 混在同一个方括号里。第一版按 `[ref=` 起头写,于是带属性的行(heading/button 这类
131
+ * 最该被推荐的)全部漏掉——真实样本进测试之后才暴露。
132
+ *
133
+ * ⚠️ **两种形状都要认**。文档和工具描述里写的是 `@eN`,但 agent-browser 实际输出的是
134
+ * `[ref=e1]`:
135
+ * `- link "Learn more" [ref=e2]`
136
+ * 第一版只认 `@eN`,于是候选推荐在真实页面上**一个都匹配不到** —— 单测全绿,实机零命中。
137
+ * 是跑了一次真的 example.com 才发现的。两种都收,并统一成 `@eN` 交给模型(那是它被教的
138
+ * 引用写法)。
139
+ */
140
+ export function parseSnapshotCandidates(snapshot: string): SnapshotCandidate[] {
141
+ const out: SnapshotCandidate[] = [];
142
+ for (const raw of snapshot.split('\n')) {
143
+ const line = raw.trim();
144
+ const match = /@e(\d+)|\bref=e(\d+)/.exec(line);
145
+ if (!match) continue;
146
+ const id = match[1] ?? match[2];
147
+ out.push({ ref: `@e${id}`, line: line.slice(0, 160) });
148
+ }
149
+ return out;
150
+ }
151
+
152
+ /**
153
+ * 归一化成可比较的词:小写、去标点、拆词。
154
+ *
155
+ * ⚠️ 连字符和下划线**必须**当分隔符。真实 CSS 选择器几乎全是 `a.learn-more-link`、
156
+ * `#btn_submit`、`[data-testid=...]` 这个形状,不拆的话整串是一个词,和快照里的
157
+ * `link "Learn more"` 永远匹配不上 —— 功能在最常见的输入上直接失效。
158
+ * 第一版漏了这两个字符,单测(用的是中文与空格分词的样本)全绿,实机零命中。
159
+ */
160
+ function tokenize(value: string): string[] {
161
+ return value
162
+ .toLowerCase()
163
+ .replace(/[#.>[\]="'`(),:{}\-_/*+~|]/g, ' ')
164
+ .split(/\s+/)
165
+ .filter((token) => token.length > 1);
166
+ }
167
+
168
+ /**
169
+ * 选择器没命中时,从快照里挑出最像目标的几个候选。
170
+ *
171
+ * 打分只用「词重叠」这一件事,不引模糊匹配库:这里要的不是排序精度,是**给模型一个
172
+ * 能立刻拿去用的 `@eN` 列表**,而不是让它对着 `selector not found` 再猜一轮。
173
+ * 排名差一点无所谓,列表里有正确答案就赢了。
174
+ */
175
+ export function suggestSelectorCandidates(
176
+ snapshot: string,
177
+ selector: string,
178
+ limit = 5,
179
+ ): SnapshotCandidate[] {
180
+ const wanted = tokenize(selector);
181
+ if (wanted.length === 0) return [];
182
+ const scored = parseSnapshotCandidates(snapshot)
183
+ .map((candidate) => {
184
+ const tokens = new Set(tokenize(candidate.line));
185
+ let score = 0;
186
+ for (const token of wanted) {
187
+ if (tokens.has(token)) score += 2;
188
+ else if (candidate.line.toLowerCase().includes(token)) score += 1;
189
+ }
190
+ return { candidate, score };
191
+ })
192
+ .filter((entry) => entry.score > 0)
193
+ .sort((a, b) => b.score - a.score)
194
+ .slice(0, limit);
195
+ return scored.map((entry) => entry.candidate);
196
+ }
197
+
198
+ /**
199
+ * 同一目标连续失败的熔断计数。
200
+ *
201
+ * 语义**刻意与 ComputerUse 的 `refuseWhenStuck` 一致**:清零的条件是「**这个动作**
202
+ * 成功了一次」,读取类动作(snapshot/getContent)成功**不清**写操作的账。
203
+ * 原因在那边写过:`失败 → snapshot → 失败 → snapshot …` 正是模型打转时最常见的形状,
204
+ * 让 snapshot 清零等于熔断永远数不到上限。
205
+ */
206
+ export class BrowserAttemptLedger {
207
+ private counts = new Map<string, number>();
208
+
209
+ private static key(conversationKey: string, action: string, selector?: string): string {
210
+ return `${conversationKey}${action}${selector ?? ''}`;
211
+ }
212
+
213
+ recordFailure(conversationKey: string, action: string, selector?: string): number {
214
+ const key = BrowserAttemptLedger.key(conversationKey, action, selector);
215
+ const next = (this.counts.get(key) ?? 0) + 1;
216
+ this.counts.set(key, next);
217
+ return next;
218
+ }
219
+
220
+ recordSuccess(conversationKey: string, action: string, selector?: string): void {
221
+ this.counts.delete(BrowserAttemptLedger.key(conversationKey, action, selector));
222
+ }
223
+
224
+ failures(conversationKey: string, action: string, selector?: string): number {
225
+ return this.counts.get(BrowserAttemptLedger.key(conversationKey, action, selector)) ?? 0;
226
+ }
227
+
228
+ /** 会话结束时清账,避免长期驻留。 */
229
+ forget(conversationKey: string): void {
230
+ for (const key of [...this.counts.keys()]) {
231
+ if (key.startsWith(`${conversationKey}`)) this.counts.delete(key);
232
+ }
233
+ }
234
+ }
235
+
236
+ /** 同一 (动作, 选择器) 连续失败到这个次数就拒绝再试。 */
237
+ export const BROWSER_FAILURE_LIMIT = 3;
238
+
239
+ export const browserAttempts = new BrowserAttemptLedger();
240
+
241
+ /** 单行最长保留字符数;超出就地截断,不因为一行长就丢掉整行的引用信息。 */
242
+ const MAX_SNAPSHOT_LINE_CHARS = 300;
243
+
244
+ /**
245
+ * 把快照收进上下文预算。
246
+ *
247
+ * 失败回执里带整份快照曾经是**最贵的一笔开销**:大页面 30k 字符 ≈ 8k token,而一次
248
+ * 失败往往连着几次。更糟的是这些 token 大半花在纯文本节点上——模型下一步要用的是
249
+ * **可引用的元素**(带 ref 的行),成段的正文对「换个目标重试」毫无帮助。
250
+ *
251
+ * 所以预算不是简单截断,是**按价值取舍**:带 ref 的行优先保留(那是可操作面),
252
+ * 纯文本行只在还有余量时补。
253
+ *
254
+ * ⚠️ 但取舍**只决定留哪些行,不决定行的顺序**。快照是一棵缩进树,把带 ref 的行
255
+ * 全部提到前面会让缩进层级与父子关系错位——模型看到的是一棵结构被打乱、看上去却
256
+ * 仍然合法的树,判断「这个按钮在哪个对话框里」时比截断更有害。所以输出严格按原序,
257
+ * 连续被删的段落就地留一条 `… 省略 N 行` 占位,让模型知道**哪里**有东西被省了。
258
+ */
259
+ export function budgetSnapshot(
260
+ snapshot: string,
261
+ opts: { maxLines?: number; maxChars?: number } = {},
262
+ ): string {
263
+ const maxLines = opts.maxLines ?? 80;
264
+ const maxChars = opts.maxChars ?? 6_000;
265
+ const lines = snapshot.split('\n').filter((line) => line.trim().length > 0);
266
+ if (lines.length <= maxLines && snapshot.length <= maxChars) return snapshot;
267
+
268
+ const hasRef = (line: string): boolean => /@e\d+|\bref=e\d+/.test(line);
269
+ const clip = (line: string): string =>
270
+ line.length <= MAX_SNAPSHOT_LINE_CHARS ? line : `${line.slice(0, MAX_SNAPSHOT_LINE_CHARS)}…`;
271
+
272
+ // 两轮**只挑不排**:先在原序上标记要留的可操作行,再用余量补纯文本行。
273
+ const keep = new Array<boolean>(lines.length).fill(false);
274
+ let kept = 0;
275
+ let chars = 0;
276
+ const take = (index: number): void => {
277
+ if (keep[index] || kept >= maxLines) return;
278
+ const cost = clip(lines[index]).length + 1;
279
+ // 放不下就跳过这一行继续看下一行——`break` 会让一条超长行连累后面所有放得下的。
280
+ if (chars + cost > maxChars) return;
281
+ keep[index] = true;
282
+ kept += 1;
283
+ chars += cost;
284
+ };
285
+ for (let i = 0; i < lines.length; i += 1) if (hasRef(lines[i])) take(i);
286
+ for (let i = 0; i < lines.length; i += 1) if (!hasRef(lines[i])) take(i);
287
+
288
+ const out: string[] = [];
289
+ let gap = 0;
290
+ const flushGap = (): void => {
291
+ if (gap > 0) out.push(` … 省略 ${gap} 行`);
292
+ gap = 0;
293
+ };
294
+ for (let i = 0; i < lines.length; i += 1) {
295
+ if (keep[i]) { flushGap(); out.push(clip(lines[i])); }
296
+ else gap += 1;
297
+ }
298
+ flushGap();
299
+
300
+ const dropped = lines.length - kept;
301
+ return (
302
+ `${out.join('\n')}\n`
303
+ + `… 为控制上下文,共省略 ${dropped} 行(按原顺序保留,优先保住可引用的元素)。`
304
+ + '需要完整页面请单独调用 snapshot,或用 getContent 读取正文。'
305
+ );
306
+ }
@@ -0,0 +1,150 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ const NEGATED_ACTION_PREFIX = /(?:不要|不需(?:要)?|不能|不可|别|禁止|避免|无需|切勿)(?:再|自动|直接|帮我|替我|进行)?[\s\S]{0,8}$|(?:do\s+not|don't|never|must\s+not|should\s+not|without)(?:\s+\w+){0,4}\s*$/i;
3
+ function hasUnnegatedAction(text, pattern) {
4
+ for (const match of text.matchAll(pattern)) {
5
+ const prefix = text.slice(Math.max(0, (match.index ?? 0) - 32), match.index);
6
+ if (!NEGATED_ACTION_PREFIX.test(prefix))
7
+ return true;
8
+ }
9
+ return false;
10
+ }
11
+ /**
12
+ * Derive only task-level grants explicitly present in the owner's message.
13
+ * Page text and model-generated arguments never call this helper, so they
14
+ * cannot expand the current browser task's authority.
15
+ */
16
+ export function inferBrowserTaskGrants(text) {
17
+ const grants = ['observe', 'interact'];
18
+ if (hasUnnegatedAction(text, /\b(?:upload|attach)\b(?!\s+(?:page|button|history|settings))|上传(?!页|页面|按钮|记录|历史|设置)|选择文件|添加文件/giu))
19
+ grants.push('upload');
20
+ if (hasUnnegatedAction(text, /\b(?:publish|submit|purchase|buy|delete|place\s+(?:an?\s+)?order|send)\b(?!\s+(?:page|button|history|record|settings))|发布(?!页|页面|按钮|记录|历史|设置)|发表(?!页|页面|按钮|记录|历史|设置)|提交(?!页|页面|按钮|记录|历史|设置)|购买|下单|删除|发送/giu))
21
+ grants.push('submit');
22
+ return grants;
23
+ }
24
+ /** Detect obvious final-action controls before they are clicked. */
25
+ export function isLikelyIrreversibleBrowserTarget(label) {
26
+ const normalized = label.replace(/\s+/g, ' ').trim();
27
+ if (!normalized)
28
+ return false;
29
+ return /(?:^|[\s"'=:_-])(?:publish|submit|purchase|buy now|place order|delete|confirm delete|send)(?:$|[\s"'=:_-])/i.test(normalized)
30
+ || /(?:发布|发表|提交|立即购买|确认购买|确认下单|删除|确认删除|发送)(?:$|[\s::))】\]])/.test(normalized);
31
+ }
32
+ /**
33
+ * Process-wide ownership registry for the shared browser profile.
34
+ *
35
+ * The Chrome profile intentionally spans conversations so login state can be
36
+ * reused. Tabs and control leases do not: every browser action must first bind
37
+ * to a conversation/run and may only target tabs claimed by that conversation.
38
+ */
39
+ export class BrowserRunRegistry {
40
+ runs = new Map();
41
+ conversationTabs = new Map();
42
+ tabOwners = new Map();
43
+ ensure(input) {
44
+ const key = this.key(input.conversationKey, input.runId ?? 'workbench');
45
+ const existing = this.runs.get(key);
46
+ if (existing) {
47
+ if (input.grants)
48
+ existing.grants = [...new Set(input.grants)];
49
+ if (input.taskGrantId !== undefined)
50
+ existing.taskGrantId = input.taskGrantId;
51
+ if (input.allowedUploadRoots)
52
+ existing.allowedUploadRoots = [...new Set(input.allowedUploadRoots)];
53
+ return existing;
54
+ }
55
+ const context = {
56
+ conversationKey: input.conversationKey,
57
+ runId: input.runId ?? randomUUID(),
58
+ profileId: input.profileId ?? 'default',
59
+ claimedTabIds: [],
60
+ grants: [...new Set(input.grants ?? ['observe', 'interact'])],
61
+ allowedUploadRoots: [...new Set(input.allowedUploadRoots ?? [])],
62
+ ...(input.taskGrantId ? { taskGrantId: input.taskGrantId } : {}),
63
+ };
64
+ this.runs.set(this.key(context.conversationKey, context.runId), context);
65
+ return context;
66
+ }
67
+ find(conversationKey, runId) {
68
+ if (runId)
69
+ return this.runs.get(this.key(conversationKey, runId));
70
+ return [...this.runs.values()].find((item) => item.conversationKey === conversationKey);
71
+ }
72
+ claimedTabs(conversationKey) {
73
+ return [...(this.conversationTabs.get(conversationKey) ?? [])];
74
+ }
75
+ ownerOf(tabId) {
76
+ return this.tabOwners.get(tabId);
77
+ }
78
+ claimTab(context, tabId) {
79
+ const owner = this.tabOwners.get(tabId);
80
+ if (owner && owner !== context.conversationKey) {
81
+ throw new Error('该浏览器标签属于另一个会话,已阻止跨会话操作');
82
+ }
83
+ this.tabOwners.set(tabId, context.conversationKey);
84
+ const claimed = this.conversationTabs.get(context.conversationKey) ?? new Set();
85
+ claimed.add(tabId);
86
+ this.conversationTabs.set(context.conversationKey, claimed);
87
+ context.claimedTabIds = [...claimed];
88
+ context.activeTabId = tabId;
89
+ }
90
+ /**
91
+ * 清掉一个会话的全部登记(标签归属 + run 上下文)。
92
+ *
93
+ * 注册表原先只有「释放单个标签」,没有会话级收口:长驻进程里每个结束的会话都会把
94
+ * 自己的标签归属永久留在表里,后来的会话再想认领同一个标签就会被判成跨会话冲突。
95
+ * 测试里先撞上(基准集跑完不清理,把 browserTabs 的用例挤挂了),但这是生产同款问题。
96
+ */
97
+ forgetConversation(conversationKey) {
98
+ for (const tabId of this.claimedTabs(conversationKey))
99
+ this.releaseTab(conversationKey, tabId);
100
+ this.conversationTabs.delete(conversationKey);
101
+ for (const [key, run] of [...this.runs.entries()]) {
102
+ if (run.conversationKey === conversationKey)
103
+ this.runs.delete(key);
104
+ }
105
+ }
106
+ releaseTab(conversationKey, tabId) {
107
+ if (this.tabOwners.get(tabId) !== conversationKey)
108
+ return;
109
+ this.tabOwners.delete(tabId);
110
+ const claimed = this.conversationTabs.get(conversationKey);
111
+ claimed?.delete(tabId);
112
+ if (claimed?.size === 0)
113
+ this.conversationTabs.delete(conversationKey);
114
+ for (const run of this.runs.values()) {
115
+ if (run.conversationKey !== conversationKey)
116
+ continue;
117
+ run.claimedTabIds = [...(claimed ?? [])];
118
+ if (run.activeTabId === tabId)
119
+ run.activeTabId = run.claimedTabIds[0];
120
+ }
121
+ }
122
+ assertTab(context, tabId) {
123
+ if (this.tabOwners.get(tabId) !== context.conversationKey) {
124
+ throw new Error('当前标签未被本会话认领,已阻止浏览器操作');
125
+ }
126
+ }
127
+ requireGrant(context, grant) {
128
+ if (!context.grants.includes(grant)) {
129
+ throw new Error(`当前浏览器任务未获得 ${grant} 权限`);
130
+ }
131
+ }
132
+ acquireLease(context) {
133
+ if (context.controlLeaseId)
134
+ return context.controlLeaseId;
135
+ context.controlLeaseId = randomUUID();
136
+ return context.controlLeaseId;
137
+ }
138
+ releaseLease(context, leaseId) {
139
+ if (leaseId && context.controlLeaseId !== leaseId)
140
+ return;
141
+ context.controlLeaseId = undefined;
142
+ }
143
+ finish(conversationKey, runId) {
144
+ this.runs.delete(this.key(conversationKey, runId));
145
+ }
146
+ key(conversationKey, runId) {
147
+ return `${conversationKey}\u0000${runId}`;
148
+ }
149
+ }
150
+ export const browserRunRegistry = new BrowserRunRegistry();
@@ -0,0 +1,179 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ export type BrowserActionGrant = 'observe' | 'interact' | 'upload' | 'submit';
4
+
5
+ const NEGATED_ACTION_PREFIX = /(?:不要|不需(?:要)?|不能|不可|别|禁止|避免|无需|切勿)(?:再|自动|直接|帮我|替我|进行)?[\s\S]{0,8}$|(?:do\s+not|don't|never|must\s+not|should\s+not|without)(?:\s+\w+){0,4}\s*$/i;
6
+
7
+ function hasUnnegatedAction(text: string, pattern: RegExp): boolean {
8
+ for (const match of text.matchAll(pattern)) {
9
+ const prefix = text.slice(Math.max(0, (match.index ?? 0) - 32), match.index);
10
+ if (!NEGATED_ACTION_PREFIX.test(prefix)) return true;
11
+ }
12
+ return false;
13
+ }
14
+
15
+ /**
16
+ * Derive only task-level grants explicitly present in the owner's message.
17
+ * Page text and model-generated arguments never call this helper, so they
18
+ * cannot expand the current browser task's authority.
19
+ */
20
+ export function inferBrowserTaskGrants(text: string): BrowserActionGrant[] {
21
+ const grants: BrowserActionGrant[] = ['observe', 'interact'];
22
+ if (hasUnnegatedAction(text, /\b(?:upload|attach)\b(?!\s+(?:page|button|history|settings))|上传(?!页|页面|按钮|记录|历史|设置)|选择文件|添加文件/giu)) grants.push('upload');
23
+ if (hasUnnegatedAction(
24
+ text,
25
+ /\b(?:publish|submit|purchase|buy|delete|place\s+(?:an?\s+)?order|send)\b(?!\s+(?:page|button|history|record|settings))|发布(?!页|页面|按钮|记录|历史|设置)|发表(?!页|页面|按钮|记录|历史|设置)|提交(?!页|页面|按钮|记录|历史|设置)|购买|下单|删除|发送/giu,
26
+ )) grants.push('submit');
27
+ return grants;
28
+ }
29
+
30
+ /** Detect obvious final-action controls before they are clicked. */
31
+ export function isLikelyIrreversibleBrowserTarget(label: string): boolean {
32
+ const normalized = label.replace(/\s+/g, ' ').trim();
33
+ if (!normalized) return false;
34
+ return /(?:^|[\s"'=:_-])(?:publish|submit|purchase|buy now|place order|delete|confirm delete|send)(?:$|[\s"'=:_-])/i.test(normalized)
35
+ || /(?:发布|发表|提交|立即购买|确认购买|确认下单|删除|确认删除|发送)(?:$|[\s::))】\]])/.test(normalized);
36
+ }
37
+
38
+ export interface BrowserRunContext {
39
+ conversationKey: string;
40
+ runId: string;
41
+ profileId: string;
42
+ claimedTabIds: string[];
43
+ activeTabId?: string;
44
+ controlLeaseId?: string;
45
+ taskGrantId?: string;
46
+ grants: BrowserActionGrant[];
47
+ allowedUploadRoots: string[];
48
+ }
49
+
50
+ type BrowserRunInput = {
51
+ conversationKey: string;
52
+ runId?: string;
53
+ profileId?: string;
54
+ taskGrantId?: string;
55
+ grants?: BrowserActionGrant[];
56
+ allowedUploadRoots?: string[];
57
+ };
58
+
59
+ /**
60
+ * Process-wide ownership registry for the shared browser profile.
61
+ *
62
+ * The Chrome profile intentionally spans conversations so login state can be
63
+ * reused. Tabs and control leases do not: every browser action must first bind
64
+ * to a conversation/run and may only target tabs claimed by that conversation.
65
+ */
66
+ export class BrowserRunRegistry {
67
+ private readonly runs = new Map<string, BrowserRunContext>();
68
+ private readonly conversationTabs = new Map<string, Set<string>>();
69
+ private readonly tabOwners = new Map<string, string>();
70
+
71
+ ensure(input: BrowserRunInput): BrowserRunContext {
72
+ const key = this.key(input.conversationKey, input.runId ?? 'workbench');
73
+ const existing = this.runs.get(key);
74
+ if (existing) {
75
+ if (input.grants) existing.grants = [...new Set(input.grants)];
76
+ if (input.taskGrantId !== undefined) existing.taskGrantId = input.taskGrantId;
77
+ if (input.allowedUploadRoots) existing.allowedUploadRoots = [...new Set(input.allowedUploadRoots)];
78
+ return existing;
79
+ }
80
+ const context: BrowserRunContext = {
81
+ conversationKey: input.conversationKey,
82
+ runId: input.runId ?? randomUUID(),
83
+ profileId: input.profileId ?? 'default',
84
+ claimedTabIds: [],
85
+ grants: [...new Set<BrowserActionGrant>(input.grants ?? ['observe', 'interact'])],
86
+ allowedUploadRoots: [...new Set(input.allowedUploadRoots ?? [])],
87
+ ...(input.taskGrantId ? { taskGrantId: input.taskGrantId } : {}),
88
+ };
89
+ this.runs.set(this.key(context.conversationKey, context.runId), context);
90
+ return context;
91
+ }
92
+
93
+ find(conversationKey: string, runId?: string): BrowserRunContext | undefined {
94
+ if (runId) return this.runs.get(this.key(conversationKey, runId));
95
+ return [...this.runs.values()].find((item) => item.conversationKey === conversationKey);
96
+ }
97
+
98
+ claimedTabs(conversationKey: string): string[] {
99
+ return [...(this.conversationTabs.get(conversationKey) ?? [])];
100
+ }
101
+
102
+ ownerOf(tabId: string): string | undefined {
103
+ return this.tabOwners.get(tabId);
104
+ }
105
+
106
+ claimTab(context: BrowserRunContext, tabId: string): void {
107
+ const owner = this.tabOwners.get(tabId);
108
+ if (owner && owner !== context.conversationKey) {
109
+ throw new Error('该浏览器标签属于另一个会话,已阻止跨会话操作');
110
+ }
111
+ this.tabOwners.set(tabId, context.conversationKey);
112
+ const claimed = this.conversationTabs.get(context.conversationKey) ?? new Set<string>();
113
+ claimed.add(tabId);
114
+ this.conversationTabs.set(context.conversationKey, claimed);
115
+ context.claimedTabIds = [...claimed];
116
+ context.activeTabId = tabId;
117
+ }
118
+
119
+ /**
120
+ * 清掉一个会话的全部登记(标签归属 + run 上下文)。
121
+ *
122
+ * 注册表原先只有「释放单个标签」,没有会话级收口:长驻进程里每个结束的会话都会把
123
+ * 自己的标签归属永久留在表里,后来的会话再想认领同一个标签就会被判成跨会话冲突。
124
+ * 测试里先撞上(基准集跑完不清理,把 browserTabs 的用例挤挂了),但这是生产同款问题。
125
+ */
126
+ forgetConversation(conversationKey: string): void {
127
+ for (const tabId of this.claimedTabs(conversationKey)) this.releaseTab(conversationKey, tabId);
128
+ this.conversationTabs.delete(conversationKey);
129
+ for (const [key, run] of [...this.runs.entries()]) {
130
+ if (run.conversationKey === conversationKey) this.runs.delete(key);
131
+ }
132
+ }
133
+
134
+ releaseTab(conversationKey: string, tabId: string): void {
135
+ if (this.tabOwners.get(tabId) !== conversationKey) return;
136
+ this.tabOwners.delete(tabId);
137
+ const claimed = this.conversationTabs.get(conversationKey);
138
+ claimed?.delete(tabId);
139
+ if (claimed?.size === 0) this.conversationTabs.delete(conversationKey);
140
+ for (const run of this.runs.values()) {
141
+ if (run.conversationKey !== conversationKey) continue;
142
+ run.claimedTabIds = [...(claimed ?? [])];
143
+ if (run.activeTabId === tabId) run.activeTabId = run.claimedTabIds[0];
144
+ }
145
+ }
146
+
147
+ assertTab(context: BrowserRunContext, tabId: string): void {
148
+ if (this.tabOwners.get(tabId) !== context.conversationKey) {
149
+ throw new Error('当前标签未被本会话认领,已阻止浏览器操作');
150
+ }
151
+ }
152
+
153
+ requireGrant(context: BrowserRunContext, grant: BrowserActionGrant): void {
154
+ if (!context.grants.includes(grant)) {
155
+ throw new Error(`当前浏览器任务未获得 ${grant} 权限`);
156
+ }
157
+ }
158
+
159
+ acquireLease(context: BrowserRunContext): string {
160
+ if (context.controlLeaseId) return context.controlLeaseId;
161
+ context.controlLeaseId = randomUUID();
162
+ return context.controlLeaseId;
163
+ }
164
+
165
+ releaseLease(context: BrowserRunContext, leaseId?: string): void {
166
+ if (leaseId && context.controlLeaseId !== leaseId) return;
167
+ context.controlLeaseId = undefined;
168
+ }
169
+
170
+ finish(conversationKey: string, runId: string): void {
171
+ this.runs.delete(this.key(conversationKey, runId));
172
+ }
173
+
174
+ private key(conversationKey: string, runId: string): string {
175
+ return `${conversationKey}\u0000${runId}`;
176
+ }
177
+ }
178
+
179
+ export const browserRunRegistry = new BrowserRunRegistry();
@@ -5,17 +5,28 @@
5
5
  */
6
6
  export class BrowserService {
7
7
  tails = new Map();
8
- async run(input) {
8
+ async exclusive(input, task) {
9
9
  const previous = this.tails.get(input.sessionName) ?? Promise.resolve();
10
10
  let release;
11
11
  const current = new Promise((resolve) => { release = resolve; });
12
- const tail = previous.then(() => current);
12
+ // 链上永远接一个已消化的 previous:任何一环 reject 都不能顺着队列传下去,
13
+ // 否则后来者在 `await previous` 处抛出。
14
+ const tail = previous.catch(() => undefined).then(() => current);
13
15
  this.tails.set(input.sessionName, tail);
14
- await previous;
16
+ // ⚠️ `await previous` **必须在 try 之内**。它曾经在 try 之外:previous 一旦 reject,
17
+ // 函数在进入 try 之前就抛出 → `release()` 永不执行 → `current` 永不 resolve →
18
+ // 后续所有该 session 的动作永久排队。症状是死锁而不是异常,排查起来极难。
19
+ // 当前 current 只 resolve 不 reject,所以实际安全 —— 但那是一条没写下来的不变量,
20
+ // 谁往链上加一个 .then 都可能打破它。把 await 挪进来,安全性就不再依赖它。
15
21
  try {
16
- if (input.signal?.aborted)
17
- return { code: -1, stdout: '', stderr: 'Browser action cancelled' };
18
- return await input.runner(input.bin, input.args, input.signal);
22
+ await previous.catch(() => undefined);
23
+ const run = async (args) => {
24
+ if (input.signal?.aborted) {
25
+ return { code: -1, stdout: '', stderr: 'Browser action cancelled' };
26
+ }
27
+ return input.runner(input.bin, args, input.signal);
28
+ };
29
+ return await task(run);
19
30
  }
20
31
  finally {
21
32
  release();
@@ -23,5 +34,8 @@ export class BrowserService {
23
34
  this.tails.delete(input.sessionName);
24
35
  }
25
36
  }
37
+ async run(input) {
38
+ return this.exclusive(input, (run) => run(input.args));
39
+ }
26
40
  }
27
41
  export const browserService = new BrowserService();