opencandle 0.5.0 → 0.6.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 (527) hide show
  1. package/README.md +164 -187
  2. package/dist/analysts/contracts.d.ts +1 -3
  3. package/dist/analysts/contracts.js +1 -11
  4. package/dist/analysts/contracts.js.map +1 -1
  5. package/dist/analysts/orchestrator.d.ts +1 -3
  6. package/dist/analysts/orchestrator.js +1 -26
  7. package/dist/analysts/orchestrator.js.map +1 -1
  8. package/dist/cli.js +30 -7
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config.d.ts +3 -3
  11. package/dist/config.js +12 -5
  12. package/dist/config.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/infra/browser.js +3 -1
  17. package/dist/infra/browser.js.map +1 -1
  18. package/dist/infra/cache.d.ts +8 -11
  19. package/dist/infra/cache.js +17 -15
  20. package/dist/infra/cache.js.map +1 -1
  21. package/dist/infra/http-client.d.ts +4 -1
  22. package/dist/infra/http-client.js +59 -6
  23. package/dist/infra/http-client.js.map +1 -1
  24. package/dist/infra/index.d.ts +3 -3
  25. package/dist/infra/index.js +3 -3
  26. package/dist/infra/index.js.map +1 -1
  27. package/dist/infra/native-dependencies.js +2 -2
  28. package/dist/infra/native-dependencies.js.map +1 -1
  29. package/dist/infra/node-version.js.map +1 -1
  30. package/dist/infra/opencandle-paths.d.ts +0 -3
  31. package/dist/infra/opencandle-paths.js +4 -11
  32. package/dist/infra/opencandle-paths.js.map +1 -1
  33. package/dist/infra/rate-limiter.js +12 -9
  34. package/dist/infra/rate-limiter.js.map +1 -1
  35. package/dist/market-state/alert-conditions.d.ts +34 -0
  36. package/dist/market-state/alert-conditions.js +23 -0
  37. package/dist/market-state/alert-conditions.js.map +1 -0
  38. package/dist/market-state/alert-runner.d.ts +55 -0
  39. package/dist/market-state/alert-runner.js +634 -0
  40. package/dist/market-state/alert-runner.js.map +1 -0
  41. package/dist/market-state/daily-report.d.ts +26 -0
  42. package/dist/market-state/daily-report.js +179 -0
  43. package/dist/market-state/daily-report.js.map +1 -0
  44. package/dist/market-state/local-automation-service.d.ts +25 -0
  45. package/dist/market-state/local-automation-service.js +119 -0
  46. package/dist/market-state/local-automation-service.js.map +1 -0
  47. package/dist/market-state/notification-delivery.d.ts +14 -0
  48. package/dist/market-state/notification-delivery.js +139 -0
  49. package/dist/market-state/notification-delivery.js.map +1 -0
  50. package/dist/market-state/resolve-for-mutation.d.ts +10 -0
  51. package/dist/market-state/resolve-for-mutation.js +15 -0
  52. package/dist/market-state/resolve-for-mutation.js.map +1 -0
  53. package/dist/market-state/resolve.d.ts +14 -0
  54. package/dist/market-state/resolve.js +89 -0
  55. package/dist/market-state/resolve.js.map +1 -0
  56. package/dist/market-state/service.d.ts +527 -0
  57. package/dist/market-state/service.js +1099 -0
  58. package/dist/market-state/service.js.map +1 -0
  59. package/dist/memory/index.d.ts +7 -7
  60. package/dist/memory/index.js +6 -6
  61. package/dist/memory/index.js.map +1 -1
  62. package/dist/memory/manager.js +11 -11
  63. package/dist/memory/manager.js.map +1 -1
  64. package/dist/memory/retrieval.js +7 -4
  65. package/dist/memory/retrieval.js.map +1 -1
  66. package/dist/memory/sqlite.js +385 -3
  67. package/dist/memory/sqlite.js.map +1 -1
  68. package/dist/memory/storage.js +1 -2
  69. package/dist/memory/storage.js.map +1 -1
  70. package/dist/memory/tool-defaults.js +64 -28
  71. package/dist/memory/tool-defaults.js.map +1 -1
  72. package/dist/memory/types.js.map +1 -1
  73. package/dist/monitor.d.ts +2 -0
  74. package/dist/monitor.js +104 -0
  75. package/dist/monitor.js.map +1 -0
  76. package/dist/onboarding/connect.js +4 -6
  77. package/dist/onboarding/connect.js.map +1 -1
  78. package/dist/onboarding/credential-interceptor.js +1 -1
  79. package/dist/onboarding/credential-interceptor.js.map +1 -1
  80. package/dist/onboarding/degradation-accumulator.js +1 -3
  81. package/dist/onboarding/degradation-accumulator.js.map +1 -1
  82. package/dist/onboarding/providers.js +3 -16
  83. package/dist/onboarding/providers.js.map +1 -1
  84. package/dist/onboarding/state.js.map +1 -1
  85. package/dist/onboarding/tool-helpers.js +1 -1
  86. package/dist/onboarding/tool-helpers.js.map +1 -1
  87. package/dist/onboarding/tool-tags.js +6 -4
  88. package/dist/onboarding/tool-tags.js.map +1 -1
  89. package/dist/onboarding/validation.js +1 -1
  90. package/dist/onboarding/validation.js.map +1 -1
  91. package/dist/pi/opencandle-extension.d.ts +8 -0
  92. package/dist/pi/opencandle-extension.js +412 -28
  93. package/dist/pi/opencandle-extension.js.map +1 -1
  94. package/dist/pi/session.d.ts +1 -1
  95. package/dist/pi/session.js +3 -1
  96. package/dist/pi/session.js.map +1 -1
  97. package/dist/pi/setup.js +8 -3
  98. package/dist/pi/setup.js.map +1 -1
  99. package/dist/pi/tool-adapter.js +5 -2
  100. package/dist/pi/tool-adapter.js.map +1 -1
  101. package/dist/prompts/context-builder.d.ts +1 -1
  102. package/dist/prompts/context-builder.js +19 -6
  103. package/dist/prompts/context-builder.js.map +1 -1
  104. package/dist/prompts/policy-cards.d.ts +1 -1
  105. package/dist/prompts/policy-cards.js +1 -1
  106. package/dist/prompts/policy-cards.js.map +1 -1
  107. package/dist/prompts/sections.d.ts +1 -1
  108. package/dist/prompts/symbol-preflight.d.ts +20 -0
  109. package/dist/prompts/symbol-preflight.js +49 -0
  110. package/dist/prompts/symbol-preflight.js.map +1 -0
  111. package/dist/prompts/workflow-prompts.d.ts +1 -1
  112. package/dist/prompts/workflow-prompts.js +54 -16
  113. package/dist/prompts/workflow-prompts.js.map +1 -1
  114. package/dist/providers/alpha-vantage.d.ts +1 -1
  115. package/dist/providers/alpha-vantage.js +26 -7
  116. package/dist/providers/alpha-vantage.js.map +1 -1
  117. package/dist/providers/coingecko.js +1 -1
  118. package/dist/providers/coingecko.js.map +1 -1
  119. package/dist/providers/errors.d.ts +5 -0
  120. package/dist/providers/errors.js +11 -0
  121. package/dist/providers/errors.js.map +1 -0
  122. package/dist/providers/exa-search.d.ts +2 -2
  123. package/dist/providers/exa-search.js +19 -11
  124. package/dist/providers/exa-search.js.map +1 -1
  125. package/dist/providers/fear-greed.js +1 -1
  126. package/dist/providers/fear-greed.js.map +1 -1
  127. package/dist/providers/finnhub.js +3 -5
  128. package/dist/providers/finnhub.js.map +1 -1
  129. package/dist/providers/fred.js +2 -2
  130. package/dist/providers/fred.js.map +1 -1
  131. package/dist/providers/index.d.ts +7 -6
  132. package/dist/providers/index.js +6 -5
  133. package/dist/providers/index.js.map +1 -1
  134. package/dist/providers/reddit.js +2 -2
  135. package/dist/providers/reddit.js.map +1 -1
  136. package/dist/providers/sec-edgar.d.ts +1 -0
  137. package/dist/providers/sec-edgar.js +12 -4
  138. package/dist/providers/sec-edgar.js.map +1 -1
  139. package/dist/providers/tradingview.d.ts +47 -0
  140. package/dist/providers/tradingview.js +275 -0
  141. package/dist/providers/tradingview.js.map +1 -0
  142. package/dist/providers/twitter.js +6 -8
  143. package/dist/providers/twitter.js.map +1 -1
  144. package/dist/providers/web-search.js +26 -12
  145. package/dist/providers/web-search.js.map +1 -1
  146. package/dist/providers/with-fallback.js +4 -2
  147. package/dist/providers/with-fallback.js.map +1 -1
  148. package/dist/providers/wrap-provider.d.ts +2 -3
  149. package/dist/providers/wrap-provider.js +14 -8
  150. package/dist/providers/wrap-provider.js.map +1 -1
  151. package/dist/providers/yahoo-finance.d.ts +1 -1
  152. package/dist/providers/yahoo-finance.js +101 -17
  153. package/dist/providers/yahoo-finance.js.map +1 -1
  154. package/dist/routing/classify-intent.d.ts +6 -0
  155. package/dist/routing/classify-intent.js +78 -7
  156. package/dist/routing/classify-intent.js.map +1 -1
  157. package/dist/routing/defaults.d.ts +1 -1
  158. package/dist/routing/entity-extractor.d.ts +1 -0
  159. package/dist/routing/entity-extractor.js +234 -29
  160. package/dist/routing/entity-extractor.js.map +1 -1
  161. package/dist/routing/fund-symbols.d.ts +2 -0
  162. package/dist/routing/fund-symbols.js +55 -0
  163. package/dist/routing/fund-symbols.js.map +1 -0
  164. package/dist/routing/horizon.d.ts +1 -0
  165. package/dist/routing/horizon.js +10 -0
  166. package/dist/routing/horizon.js.map +1 -0
  167. package/dist/routing/index.d.ts +10 -10
  168. package/dist/routing/index.js +6 -6
  169. package/dist/routing/index.js.map +1 -1
  170. package/dist/routing/planning.d.ts +1 -1
  171. package/dist/routing/planning.js +65 -34
  172. package/dist/routing/planning.js.map +1 -1
  173. package/dist/routing/route-manifest.d.ts +2 -2
  174. package/dist/routing/route-manifest.js +25 -4
  175. package/dist/routing/route-manifest.js.map +1 -1
  176. package/dist/routing/router-llm-client.js.map +1 -1
  177. package/dist/routing/router-prompt.js +7 -9
  178. package/dist/routing/router-prompt.js.map +1 -1
  179. package/dist/routing/router-types.d.ts +1 -0
  180. package/dist/routing/router.js +137 -22
  181. package/dist/routing/router.js.map +1 -1
  182. package/dist/routing/slot-resolver.d.ts +1 -1
  183. package/dist/routing/slot-resolver.js +2 -4
  184. package/dist/routing/slot-resolver.js.map +1 -1
  185. package/dist/routing/symbol-disambiguator.d.ts +11 -0
  186. package/dist/routing/symbol-disambiguator.js +52 -0
  187. package/dist/routing/symbol-disambiguator.js.map +1 -0
  188. package/dist/routing/turn-context.d.ts +1 -1
  189. package/dist/routing/turn-context.js +1 -1
  190. package/dist/routing/turn-context.js.map +1 -1
  191. package/dist/routing/types.d.ts +2 -0
  192. package/dist/runtime/answer-contracts.js +36 -8
  193. package/dist/runtime/answer-contracts.js.map +1 -1
  194. package/dist/runtime/artifact-contracts.js.map +1 -1
  195. package/dist/runtime/planning-evidence.js +47 -26
  196. package/dist/runtime/planning-evidence.js.map +1 -1
  197. package/dist/runtime/prompt-step.d.ts +1 -9
  198. package/dist/runtime/prompt-step.js +0 -10
  199. package/dist/runtime/prompt-step.js.map +1 -1
  200. package/dist/runtime/run-context.d.ts +5 -2
  201. package/dist/runtime/run-context.js +8 -1
  202. package/dist/runtime/run-context.js.map +1 -1
  203. package/dist/runtime/session-coordinator.d.ts +13 -5
  204. package/dist/runtime/session-coordinator.js +160 -20
  205. package/dist/runtime/session-coordinator.js.map +1 -1
  206. package/dist/runtime/session-title.d.ts +14 -0
  207. package/dist/runtime/session-title.js +50 -0
  208. package/dist/runtime/session-title.js.map +1 -0
  209. package/dist/runtime/tool-defaults-wrapper.js +1 -3
  210. package/dist/runtime/tool-defaults-wrapper.js.map +1 -1
  211. package/dist/runtime/validation.js.map +1 -1
  212. package/dist/runtime/workflow-events.js.map +1 -1
  213. package/dist/runtime/workflow-runner.d.ts +3 -3
  214. package/dist/runtime/workflow-runner.js +1 -1
  215. package/dist/runtime/workflow-runner.js.map +1 -1
  216. package/dist/sentiment/adapters/finnhub.d.ts +1 -1
  217. package/dist/sentiment/adapters/finnhub.js +6 -1
  218. package/dist/sentiment/adapters/finnhub.js.map +1 -1
  219. package/dist/sentiment/adapters/reddit.d.ts +2 -2
  220. package/dist/sentiment/adapters/twitter.d.ts +1 -1
  221. package/dist/sentiment/adapters/web.d.ts +1 -1
  222. package/dist/sentiment/index.d.ts +9 -11
  223. package/dist/sentiment/index.js +9 -20
  224. package/dist/sentiment/index.js.map +1 -1
  225. package/dist/sentiment/keywords.js +26 -4
  226. package/dist/sentiment/keywords.js.map +1 -1
  227. package/dist/sentiment/pipeline.d.ts +2 -2
  228. package/dist/sentiment/pipeline.js +1 -1
  229. package/dist/sentiment/pipeline.js.map +1 -1
  230. package/dist/sentiment/scorer.js +1 -1
  231. package/dist/sentiment/store.d.ts +1 -1
  232. package/dist/sentiment/store.js +1 -1
  233. package/dist/sentiment/store.js.map +1 -1
  234. package/dist/sentiment/trends.d.ts +1 -1
  235. package/dist/sentiment/trends.js.map +1 -1
  236. package/dist/sentiment/types.js.map +1 -1
  237. package/dist/system-prompt.js +3 -2
  238. package/dist/system-prompt.js.map +1 -1
  239. package/dist/tool-kit.d.ts +7 -7
  240. package/dist/tool-kit.js +4 -4
  241. package/dist/tool-kit.js.map +1 -1
  242. package/dist/tools/fundamentals/company-overview.js +11 -6
  243. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  244. package/dist/tools/fundamentals/comps.js +18 -9
  245. package/dist/tools/fundamentals/comps.js.map +1 -1
  246. package/dist/tools/fundamentals/dcf.js +23 -11
  247. package/dist/tools/fundamentals/dcf.js.map +1 -1
  248. package/dist/tools/fundamentals/earnings.js +8 -3
  249. package/dist/tools/fundamentals/earnings.js.map +1 -1
  250. package/dist/tools/fundamentals/financials.js +8 -3
  251. package/dist/tools/fundamentals/financials.js.map +1 -1
  252. package/dist/tools/fundamentals/sec-filings.js +21 -6
  253. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  254. package/dist/tools/index.d.ts +23 -19
  255. package/dist/tools/index.js +51 -39
  256. package/dist/tools/index.js.map +1 -1
  257. package/dist/tools/interaction/ask-user.js +15 -3
  258. package/dist/tools/interaction/ask-user.js.map +1 -1
  259. package/dist/tools/interaction/twitter-login.js +13 -3
  260. package/dist/tools/interaction/twitter-login.js.map +1 -1
  261. package/dist/tools/macro/fear-greed.js.map +1 -1
  262. package/dist/tools/macro/fred-data.d.ts +1 -1
  263. package/dist/tools/macro/fred-data.js +17 -6
  264. package/dist/tools/macro/fred-data.js.map +1 -1
  265. package/dist/tools/market/crypto-history.js +3 -1
  266. package/dist/tools/market/crypto-history.js.map +1 -1
  267. package/dist/tools/market/crypto-price.js +3 -1
  268. package/dist/tools/market/crypto-price.js.map +1 -1
  269. package/dist/tools/market/screen-stocks.d.ts +18 -0
  270. package/dist/tools/market/screen-stocks.js +252 -0
  271. package/dist/tools/market/screen-stocks.js.map +1 -0
  272. package/dist/tools/market/search-ticker.js +160 -8
  273. package/dist/tools/market/search-ticker.js.map +1 -1
  274. package/dist/tools/market/stock-history.d.ts +2 -2
  275. package/dist/tools/market/stock-history.js +26 -7
  276. package/dist/tools/market/stock-history.js.map +1 -1
  277. package/dist/tools/market/stock-quote.js +5 -3
  278. package/dist/tools/market/stock-quote.js.map +1 -1
  279. package/dist/tools/options/greeks.js +1 -1
  280. package/dist/tools/options/greeks.js.map +1 -1
  281. package/dist/tools/options/option-chain.js +19 -6
  282. package/dist/tools/options/option-chain.js.map +1 -1
  283. package/dist/tools/portfolio/alerts.d.ts +15 -0
  284. package/dist/tools/portfolio/alerts.js +357 -0
  285. package/dist/tools/portfolio/alerts.js.map +1 -0
  286. package/dist/tools/portfolio/correlation.d.ts +1 -1
  287. package/dist/tools/portfolio/correlation.js +33 -13
  288. package/dist/tools/portfolio/correlation.js.map +1 -1
  289. package/dist/tools/portfolio/daily-report.d.ts +8 -0
  290. package/dist/tools/portfolio/daily-report.js +83 -0
  291. package/dist/tools/portfolio/daily-report.js.map +1 -0
  292. package/dist/tools/portfolio/holdings-overlap.js +10 -3
  293. package/dist/tools/portfolio/holdings-overlap.js.map +1 -1
  294. package/dist/tools/portfolio/notifications.d.ts +7 -0
  295. package/dist/tools/portfolio/notifications.js +43 -0
  296. package/dist/tools/portfolio/notifications.js.map +1 -0
  297. package/dist/tools/portfolio/predictions.d.ts +12 -6
  298. package/dist/tools/portfolio/predictions.js +337 -87
  299. package/dist/tools/portfolio/predictions.js.map +1 -1
  300. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  301. package/dist/tools/portfolio/risk-analysis.js +45 -6
  302. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  303. package/dist/tools/portfolio/tracker.d.ts +4 -3
  304. package/dist/tools/portfolio/tracker.js +246 -101
  305. package/dist/tools/portfolio/tracker.js.map +1 -1
  306. package/dist/tools/portfolio/watchlist.d.ts +6 -4
  307. package/dist/tools/portfolio/watchlist.js +208 -108
  308. package/dist/tools/portfolio/watchlist.js.map +1 -1
  309. package/dist/tools/sentiment/reddit-sentiment.js +23 -10
  310. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  311. package/dist/tools/sentiment/sentiment-summary.js +15 -13
  312. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  313. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  314. package/dist/tools/sentiment/sentiment-trend.js +12 -2
  315. package/dist/tools/sentiment/sentiment-trend.js.map +1 -1
  316. package/dist/tools/sentiment/twitter-sentiment.js +12 -5
  317. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  318. package/dist/tools/sentiment/untrusted-text.d.ts +2 -0
  319. package/dist/tools/sentiment/untrusted-text.js +17 -0
  320. package/dist/tools/sentiment/untrusted-text.js.map +1 -0
  321. package/dist/tools/sentiment/web-search.js +9 -13
  322. package/dist/tools/sentiment/web-search.js.map +1 -1
  323. package/dist/tools/sentiment/web-sentiment.js +15 -3
  324. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  325. package/dist/tools/technical/backtest.d.ts +1 -1
  326. package/dist/tools/technical/backtest.js +27 -20
  327. package/dist/tools/technical/backtest.js.map +1 -1
  328. package/dist/tools/technical/indicators.js +23 -5
  329. package/dist/tools/technical/indicators.js.map +1 -1
  330. package/dist/types/index.d.ts +3 -3
  331. package/dist/types/index.js.map +1 -1
  332. package/dist/types/market.d.ts +1 -0
  333. package/dist/types/portfolio.d.ts +14 -4
  334. package/dist/workflows/compare-assets.d.ts +0 -3
  335. package/dist/workflows/compare-assets.js +20 -11
  336. package/dist/workflows/compare-assets.js.map +1 -1
  337. package/dist/workflows/index.d.ts +3 -4
  338. package/dist/workflows/index.js +3 -3
  339. package/dist/workflows/index.js.map +1 -1
  340. package/dist/workflows/options-screener.d.ts +0 -3
  341. package/dist/workflows/options-screener.js +4 -11
  342. package/dist/workflows/options-screener.js.map +1 -1
  343. package/dist/workflows/portfolio-builder.d.ts +0 -3
  344. package/dist/workflows/portfolio-builder.js +0 -8
  345. package/dist/workflows/portfolio-builder.js.map +1 -1
  346. package/gui/server/ask-user-bridge.ts +1 -1
  347. package/gui/server/automation-heartbeat.ts +97 -0
  348. package/gui/server/background-quotes.ts +97 -1
  349. package/gui/server/chat-event-adapter.ts +32 -10
  350. package/gui/server/chat-run-session.ts +16 -0
  351. package/gui/server/invoke-tool.ts +144 -1
  352. package/gui/server/live-chat-event-adapter.ts +21 -6
  353. package/gui/server/market-state-api.ts +315 -0
  354. package/gui/server/model-setup.ts +149 -2
  355. package/gui/server/private-api-access.ts +62 -0
  356. package/gui/server/projector.ts +12 -7
  357. package/gui/server/prompt-observation.ts +4 -7
  358. package/gui/server/quote-snapshot-store.ts +50 -0
  359. package/gui/server/server.ts +200 -451
  360. package/gui/server/session-actions.ts +186 -1
  361. package/gui/server/shutdown.ts +47 -0
  362. package/gui/server/tool-invoke-ack.ts +49 -0
  363. package/gui/server/tool-metadata.ts +23 -10
  364. package/gui/server/websocket.ts +13 -3
  365. package/gui/server/writer-lock.ts +6 -2
  366. package/gui/server/ws-hub.ts +292 -0
  367. package/gui/shared/chat-events.ts +16 -1
  368. package/gui/shared/event-reducer.ts +24 -6
  369. package/gui/web/dist/assets/CatalogOverlay-eJ2cBk33.js +1 -0
  370. package/gui/web/dist/assets/index-2KZtKBmu.css +1 -0
  371. package/gui/web/dist/assets/index-CveNgtDg.js +69 -0
  372. package/gui/web/dist/index.html +2 -2
  373. package/package.json +5 -1
  374. package/src/analysts/contracts.ts +10 -23
  375. package/src/analysts/orchestrator.ts +8 -43
  376. package/src/cli.ts +35 -12
  377. package/src/config.ts +17 -9
  378. package/src/index.ts +1 -1
  379. package/src/infra/browser.ts +3 -1
  380. package/src/infra/cache.ts +41 -30
  381. package/src/infra/http-client.ts +72 -6
  382. package/src/infra/index.ts +7 -10
  383. package/src/infra/native-dependencies.ts +8 -3
  384. package/src/infra/node-version.ts +3 -1
  385. package/src/infra/opencandle-paths.ts +3 -14
  386. package/src/infra/rate-limiter.ts +22 -19
  387. package/src/market-state/alert-conditions.ts +82 -0
  388. package/src/market-state/alert-runner.ts +863 -0
  389. package/src/market-state/daily-report.ts +247 -0
  390. package/src/market-state/local-automation-service.ts +162 -0
  391. package/src/market-state/notification-delivery.ts +158 -0
  392. package/src/market-state/resolve-for-mutation.ts +24 -0
  393. package/src/market-state/resolve.ts +112 -0
  394. package/src/market-state/service.ts +2344 -0
  395. package/src/memory/index.ts +7 -7
  396. package/src/memory/manager.ts +14 -16
  397. package/src/memory/retrieval.ts +8 -7
  398. package/src/memory/sqlite.ts +407 -6
  399. package/src/memory/storage.ts +5 -15
  400. package/src/memory/tool-defaults.ts +60 -39
  401. package/src/memory/types.ts +3 -3
  402. package/src/monitor.ts +121 -0
  403. package/src/onboarding/connect.ts +10 -33
  404. package/src/onboarding/credential-interceptor.ts +3 -15
  405. package/src/onboarding/degradation-accumulator.ts +1 -3
  406. package/src/onboarding/providers.ts +9 -40
  407. package/src/onboarding/state.ts +4 -15
  408. package/src/onboarding/tool-helpers.ts +2 -9
  409. package/src/onboarding/tool-tags.ts +6 -6
  410. package/src/onboarding/validation.ts +14 -20
  411. package/src/pi/opencandle-extension.ts +529 -85
  412. package/src/pi/session.ts +7 -5
  413. package/src/pi/setup.ts +61 -43
  414. package/src/pi/tool-adapter.ts +5 -2
  415. package/src/prompts/context-builder.ts +23 -12
  416. package/src/prompts/policy-cards.ts +2 -2
  417. package/src/prompts/sections.ts +1 -1
  418. package/src/prompts/symbol-preflight.ts +80 -0
  419. package/src/prompts/workflow-prompts.ts +77 -28
  420. package/src/providers/alpha-vantage.ts +58 -39
  421. package/src/providers/coingecko.ts +2 -5
  422. package/src/providers/errors.ts +9 -0
  423. package/src/providers/exa-search.ts +24 -22
  424. package/src/providers/fear-greed.ts +1 -1
  425. package/src/providers/finnhub.ts +7 -6
  426. package/src/providers/fred.ts +3 -3
  427. package/src/providers/index.ts +14 -6
  428. package/src/providers/reddit.ts +17 -6
  429. package/src/providers/sec-edgar.ts +20 -6
  430. package/src/providers/tradingview.ts +399 -0
  431. package/src/providers/twitter.ts +6 -8
  432. package/src/providers/web-search.ts +30 -20
  433. package/src/providers/with-fallback.ts +8 -7
  434. package/src/providers/wrap-provider.ts +15 -10
  435. package/src/providers/yahoo-finance.ts +140 -35
  436. package/src/routing/classify-intent.ts +101 -10
  437. package/src/routing/defaults.ts +1 -1
  438. package/src/routing/entity-extractor.ts +287 -38
  439. package/src/routing/fund-symbols.ts +58 -0
  440. package/src/routing/horizon.ts +7 -0
  441. package/src/routing/index.ts +48 -48
  442. package/src/routing/planning.ts +144 -53
  443. package/src/routing/route-manifest.ts +37 -15
  444. package/src/routing/router-llm-client.ts +4 -4
  445. package/src/routing/router-prompt.ts +15 -19
  446. package/src/routing/router-types.ts +2 -5
  447. package/src/routing/router.ts +251 -53
  448. package/src/routing/slot-resolver.ts +34 -11
  449. package/src/routing/symbol-disambiguator.ts +72 -0
  450. package/src/routing/turn-context.ts +6 -9
  451. package/src/routing/types.ts +2 -0
  452. package/src/runtime/answer-contracts.ts +82 -43
  453. package/src/runtime/artifact-contracts.ts +2 -1
  454. package/src/runtime/planning-evidence.ts +157 -66
  455. package/src/runtime/prompt-step.ts +1 -16
  456. package/src/runtime/run-context.ts +12 -2
  457. package/src/runtime/session-coordinator.ts +238 -63
  458. package/src/runtime/session-title.ts +60 -0
  459. package/src/runtime/tool-defaults-wrapper.ts +1 -3
  460. package/src/runtime/validation.ts +1 -4
  461. package/src/runtime/workflow-events.ts +7 -7
  462. package/src/runtime/workflow-runner.ts +5 -11
  463. package/src/sentiment/adapters/finnhub.ts +7 -2
  464. package/src/sentiment/adapters/reddit.ts +2 -2
  465. package/src/sentiment/adapters/twitter.ts +1 -1
  466. package/src/sentiment/adapters/web.ts +1 -1
  467. package/src/sentiment/index.ts +16 -26
  468. package/src/sentiment/keywords.ts +26 -4
  469. package/src/sentiment/pipeline.ts +15 -4
  470. package/src/sentiment/scorer.ts +1 -1
  471. package/src/sentiment/store.ts +2 -2
  472. package/src/sentiment/trends.ts +9 -3
  473. package/src/sentiment/types.ts +5 -4
  474. package/src/system-prompt.ts +3 -2
  475. package/src/tool-kit.ts +10 -9
  476. package/src/tools/fundamentals/company-overview.ts +19 -9
  477. package/src/tools/fundamentals/comps.ts +68 -55
  478. package/src/tools/fundamentals/dcf.ts +145 -95
  479. package/src/tools/fundamentals/earnings.ts +16 -6
  480. package/src/tools/fundamentals/financials.ts +16 -7
  481. package/src/tools/fundamentals/sec-filings.ts +37 -16
  482. package/src/tools/index.ts +51 -39
  483. package/src/tools/interaction/ask-user.ts +22 -10
  484. package/src/tools/interaction/twitter-login.ts +17 -5
  485. package/src/tools/macro/fear-greed.ts +1 -1
  486. package/src/tools/macro/fred-data.ts +58 -46
  487. package/src/tools/market/crypto-history.ts +8 -3
  488. package/src/tools/market/crypto-price.ts +6 -6
  489. package/src/tools/market/screen-stocks.ts +279 -0
  490. package/src/tools/market/search-ticker.ts +218 -17
  491. package/src/tools/market/stock-history.ts +37 -12
  492. package/src/tools/market/stock-quote.ts +10 -7
  493. package/src/tools/options/greeks.ts +5 -5
  494. package/src/tools/options/option-chain.ts +41 -17
  495. package/src/tools/portfolio/alerts.ts +457 -0
  496. package/src/tools/portfolio/correlation.ts +47 -20
  497. package/src/tools/portfolio/daily-report.ts +101 -0
  498. package/src/tools/portfolio/holdings-overlap.ts +31 -15
  499. package/src/tools/portfolio/notifications.ts +45 -0
  500. package/src/tools/portfolio/predictions.ts +406 -106
  501. package/src/tools/portfolio/risk-analysis.ts +46 -7
  502. package/src/tools/portfolio/tracker.ts +270 -109
  503. package/src/tools/portfolio/watchlist.ts +250 -121
  504. package/src/tools/sentiment/reddit-sentiment.ts +50 -24
  505. package/src/tools/sentiment/sentiment-summary.ts +62 -41
  506. package/src/tools/sentiment/sentiment-trend.ts +24 -7
  507. package/src/tools/sentiment/twitter-sentiment.ts +22 -15
  508. package/src/tools/sentiment/untrusted-text.ts +21 -0
  509. package/src/tools/sentiment/web-search.ts +21 -18
  510. package/src/tools/sentiment/web-sentiment.ts +26 -10
  511. package/src/tools/technical/backtest.ts +32 -22
  512. package/src/tools/technical/indicators.ts +39 -14
  513. package/src/types/index.ts +8 -3
  514. package/src/types/market.ts +1 -0
  515. package/src/types/portfolio.ts +14 -4
  516. package/src/types/sentiment.ts +2 -2
  517. package/src/workflows/compare-assets.ts +33 -21
  518. package/src/workflows/index.ts +3 -4
  519. package/src/workflows/options-screener.ts +27 -29
  520. package/src/workflows/portfolio-builder.ts +34 -27
  521. package/dist/workflows/types.d.ts +0 -4
  522. package/dist/workflows/types.js +0 -2
  523. package/dist/workflows/types.js.map +0 -1
  524. package/gui/web/dist/assets/CatalogOverlay-Bmp6Knu7.js +0 -1
  525. package/gui/web/dist/assets/index-Bxt9QpLX.css +0 -1
  526. package/gui/web/dist/assets/index-CZ9DHZYy.js +0 -67
  527. package/src/workflows/types.ts +0 -4
@@ -1,23 +1,39 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
+ buildComprehensiveAnalysisDefinition,
3
4
  isAnalysisRequest,
4
5
  normalizeSymbol,
5
6
  } from "../analysts/orchestrator.js";
6
- import { buildComprehensiveAnalysisDefinition } from "../analysts/orchestrator.js";
7
7
  import { getConfig } from "../config.js";
8
+ import type { InstrumentCandidate } from "../market-state/resolve.js";
9
+ import { runProviderConnect } from "../onboarding/connect.js";
10
+ import { resolveCredentialRequired } from "../onboarding/credential-interceptor.js";
11
+ import { createDegradationAccumulator } from "../onboarding/degradation-accumulator.js";
12
+ import { promptUser } from "../onboarding/prompt-user.js";
13
+ import { getProvider, type ProviderId } from "../onboarding/providers.js";
8
14
  import {
15
+ loadOnboardingState,
16
+ markProviderNeverAsk,
17
+ markProviderSnoozed,
18
+ markWelcomeShown,
19
+ saveOnboardingState,
20
+ shouldShowWelcome,
21
+ } from "../onboarding/state.js";
22
+ import { buildConnectedTag, buildSkippedTag, parseToolTag } from "../onboarding/tool-tags.js";
23
+ import { DISCLAIMER_TEXT } from "../prompts/disclaimer.js";
24
+ import { formatPreflightDropAnnotation, preflightSymbols } from "../prompts/symbol-preflight.js";
25
+ import { buildAssumptionsBlockFromRouter } from "../prompts/workflow-prompts.js";
26
+ import {
27
+ buildResolvedTurnContext,
9
28
  classifyWithLegacyRules,
10
29
  createPiAiRouterClient,
30
+ hasFinanceSignals,
11
31
  resolveOptionsScreenerSlots,
12
32
  resolvePortfolioSlots,
13
33
  route as routeLlm,
14
- buildResolvedTurnContext,
15
34
  } from "../routing/index.js";
16
- import type {
17
- RouterInputContext,
18
- RouterLlmClient,
19
- RouterOutput,
20
- } from "../routing/router-types.js";
35
+ import type { RouterInputContext, RouterLlmClient, RouterOutput } from "../routing/router-types.js";
36
+ import { disambiguateSymbols } from "../routing/symbol-disambiguator.js";
21
37
  import type { ResolvedTurnContext } from "../routing/turn-context.js";
22
38
  import type {
23
39
  CompareAssetsSlots,
@@ -25,35 +41,17 @@ import type {
25
41
  SlotResolution,
26
42
  SlotSource,
27
43
  } from "../routing/types.js";
28
- import { buildAssumptionsBlockFromRouter } from "../prompts/workflow-prompts.js";
44
+ import { SessionCoordinator } from "../runtime/session-coordinator.js";
45
+ import { generateSessionTitle } from "../runtime/session-title.js";
46
+ import { registerAskUserTool } from "../tools/interaction/ask-user.js";
47
+ import { registerTwitterLoginTool } from "../tools/interaction/twitter-login.js";
48
+ import type { AskUserHandler } from "../types/index.js";
29
49
  import {
30
- buildPortfolioWorkflowDefinition,
31
- buildOptionsScreenerWorkflowDefinition,
32
50
  buildCompareAssetsWorkflowDefinition,
51
+ buildOptionsScreenerWorkflowDefinition,
52
+ buildPortfolioWorkflowDefinition,
33
53
  } from "../workflows/index.js";
34
54
  import { getOpenCandleToolDefinitions } from "./tool-adapter.js";
35
- import { registerAskUserTool } from "../tools/interaction/ask-user.js";
36
- import { registerTwitterLoginTool } from "../tools/interaction/twitter-login.js";
37
- import { SessionCoordinator } from "../runtime/session-coordinator.js";
38
- import {
39
- getProvider,
40
- type ProviderId,
41
- } from "../onboarding/providers.js";
42
- import {
43
- loadOnboardingState,
44
- saveOnboardingState,
45
- markProviderSnoozed,
46
- markProviderNeverAsk,
47
- markWelcomeShown,
48
- shouldShowWelcome,
49
- } from "../onboarding/state.js";
50
- import { parseToolTag, buildSkippedTag, buildConnectedTag } from "../onboarding/tool-tags.js";
51
- import { resolveCredentialRequired } from "../onboarding/credential-interceptor.js";
52
- import { createDegradationAccumulator } from "../onboarding/degradation-accumulator.js";
53
- import { promptUser } from "../onboarding/prompt-user.js";
54
- import { runProviderConnect } from "../onboarding/connect.js";
55
- import type { AskUserHandler } from "../types/index.js";
56
- import { DISCLAIMER_TEXT } from "../prompts/disclaimer.js";
57
55
 
58
56
  export interface OpenCandleExtensionOptions {
59
57
  askUserHandler?: AskUserHandler;
@@ -62,11 +60,27 @@ export interface OpenCandleExtensionOptions {
62
60
  * of the pi-ai-backed default. Intended for tests + offline eval runners.
63
61
  */
64
62
  routerLlmClient?: RouterLlmClient;
63
+ symbolSearch?: (query: string) => Promise<InstrumentCandidate[]>;
64
+ /**
65
+ * Optional completion function for LLM session titles. When omitted, the
66
+ * extension resolves a pi-ai client from the session's current model.
67
+ * Intended for tests + offline runners.
68
+ */
69
+ titleCompletion?: (prompt: string) => Promise<string>;
65
70
  }
66
71
 
67
- export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCandleExtensionOptions): void {
72
+ export default function openCandleExtension(
73
+ pi: ExtensionAPI,
74
+ options?: OpenCandleExtensionOptions,
75
+ ): void {
68
76
  const coordinator = new SessionCoordinator();
69
77
 
78
+ // Workflow transforms replace the user's turn with the expanded prompt; this
79
+ // marker lets the GUI render the user's original words instead.
80
+ const markOriginalInput = (original: string): void => {
81
+ pi.appendEntry("opencandle-user-input", { original });
82
+ };
83
+
70
84
  // Credential-interception state. Lifetime:
71
85
  // `sessionPromptedSet` — cleared on session_start, persists across turns
72
86
  // within a session so users don't get re-prompted after picking
@@ -85,6 +99,10 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
85
99
  const degradationAccumulator = createDegradationAccumulator();
86
100
  let activeToolSnapshot: string[] | null = null;
87
101
  let currentRouteToolContext: ResolvedTurnContext | null = null;
102
+ // LLM session-title state: one title attempt per session per process.
103
+ // Reset on session_start; set before the (async) title call fires so
104
+ // overlapping turn_end events cannot double-title.
105
+ let sessionTitleAttempted = false;
88
106
 
89
107
  // Register tools
90
108
  for (const tool of getOpenCandleToolDefinitions()) {
@@ -102,7 +120,9 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
102
120
  ctx.ui.notify("Usage: /analyze <ticker>", "warning");
103
121
  return;
104
122
  }
105
- const definition = buildComprehensiveAnalysisDefinition(symbol, { debate: getConfig().debate });
123
+ const definition = buildComprehensiveAnalysisDefinition(symbol, {
124
+ debate: getConfig().debate,
125
+ });
106
126
  coordinator.executeWorkflow(pi, definition, ctx);
107
127
  },
108
128
  });
@@ -165,10 +185,7 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
165
185
  const all = listAllProviders()
166
186
  .map((p) => ` ${p.displayName} (${p.aliases.join(", ")})`)
167
187
  .join("\n");
168
- ctx.ui.notify(
169
- `Unknown provider: "${trimmed}". Available:\n${all}`,
170
- "warning",
171
- );
188
+ ctx.ui.notify(`Unknown provider: "${trimmed}". Available:\n${all}`, "warning");
172
189
  return;
173
190
  }
174
191
  if (Array.isArray(resolved)) {
@@ -199,6 +216,7 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
199
216
  coordinator.initSession(ctx.sessionManager.getSessionId());
200
217
  sessionPromptedSet.clear();
201
218
  hardPromptFiredInWorkflow = false;
219
+ sessionTitleAttempted = false;
202
220
 
203
221
  if (!ctx.hasUI) return;
204
222
  // Pin the user-facing disclaimer in the UI footer for the entire session.
@@ -266,8 +284,7 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
266
284
  // footer status pinned at session_start is the primary user-visible channel.
267
285
  pi.on("turn_end", async (event) => {
268
286
  const msg = event.message;
269
- const isFinalAssistantTurn =
270
- msg.role === "assistant" && msg.stopReason === "stop";
287
+ const isFinalAssistantTurn = msg.role === "assistant" && msg.stopReason === "stop";
271
288
  if (isFinalAssistantTurn) {
272
289
  pi.appendEntry("opencandle-disclaimer", { text: DISCLAIMER_TEXT });
273
290
  restoreRouteToolScope();
@@ -282,6 +299,82 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
282
299
  degradationAccumulator.reset();
283
300
  });
284
301
 
302
+ // LLM session titles. After the first completed user↔assistant exchange,
303
+ // replace the placeholder session name (the raw first prompt, set by the
304
+ // GUI server / Pi's firstMessage fallback) with a short model-written
305
+ // summary. Manual renames are left alone, and each session is attempted at
306
+ // most once per process. Model failures are swallowed (placeholder stays)
307
+ // but recorded as an `opencandle-title-error` entry for observability.
308
+ pi.on("turn_end", async (event, ctx) => {
309
+ if (sessionTitleAttempted) return;
310
+ const msg = event.message as { role?: unknown; stopReason?: unknown };
311
+ if (msg?.role !== "assistant" || msg?.stopReason !== "stop") return;
312
+ const sessionManager = ctx?.sessionManager;
313
+ if (typeof sessionManager?.getBranch !== "function") return;
314
+
315
+ const branch = sessionManager.getBranch();
316
+ let firstUserText: string | null = null;
317
+ let firstAssistantText: string | null = null;
318
+ let originalInput: string | null = null;
319
+ for (const entry of branch) {
320
+ if (
321
+ originalInput === null &&
322
+ entry.type === "custom" &&
323
+ (entry as { customType?: unknown }).customType === "opencandle-user-input"
324
+ ) {
325
+ const original = (entry as { data?: { original?: unknown } }).data?.original;
326
+ if (typeof original === "string" && original.trim().length > 0) {
327
+ originalInput = original;
328
+ }
329
+ continue;
330
+ }
331
+ if (entry.type !== "message") continue;
332
+ const message = (entry as { message?: { role?: unknown; content?: unknown } }).message;
333
+ const text = extractMessageText(message?.content);
334
+ if (text.trim().length === 0) continue;
335
+ if (firstUserText === null && message?.role === "user") firstUserText = text;
336
+ if (firstAssistantText === null && message?.role === "assistant") {
337
+ firstAssistantText = text;
338
+ }
339
+ if (firstUserText !== null && firstAssistantText !== null) break;
340
+ }
341
+ // Fall back to the just-finished assistant message when the branch has
342
+ // not surfaced it yet at turn_end time.
343
+ if (firstAssistantText === null) {
344
+ const eventText = extractMessageText((msg as { content?: unknown }).content);
345
+ if (eventText.trim().length > 0) firstAssistantText = eventText;
346
+ }
347
+ if (firstUserText === null || firstAssistantText === null) return;
348
+
349
+ const userText = originalInput ?? firstUserText;
350
+ // A manual rename must be left alone — only replace the placeholder
351
+ // (unset, the raw first prompt, or the GUI's "first 77 chars + ..." form).
352
+ if (!isPlaceholderSessionName(pi.getSessionName(), [userText, firstUserText])) return;
353
+
354
+ const completion =
355
+ options?.titleCompletion ??
356
+ (() => {
357
+ const client = options?.routerLlmClient ?? resolveRouterLlmClient(ctx);
358
+ return client ? (prompt: string) => client.complete(prompt) : null;
359
+ })();
360
+ if (!completion) return;
361
+
362
+ sessionTitleAttempted = true;
363
+ try {
364
+ const title = await generateSessionTitle(
365
+ { userText, assistantText: firstAssistantText.slice(0, 500) },
366
+ completion,
367
+ );
368
+ if (title !== null) {
369
+ pi.setSessionName(title);
370
+ }
371
+ } catch (err) {
372
+ pi.appendEntry("opencandle-title-error", {
373
+ message: err instanceof Error ? err.message : String(err),
374
+ });
375
+ }
376
+ });
377
+
285
378
  // Intercept tool results for credential-required and soft-degraded tags.
286
379
  pi.on("tool_result", async (event, ctx) => {
287
380
  // First pass: record any soft-degradation tags in the per-turn accumulator
@@ -346,10 +439,9 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
346
439
  // action === "prompt": pause and ask the user via promptUser.
347
440
  const descriptor = getProvider(parsed.provider);
348
441
  const connectLabel = `Connect now — ${descriptor.instructionsHint}`;
349
- const continueLabel =
350
- descriptor.fallbackDescription
351
- ? `Continue with ${descriptor.fallbackDescription} for this run`
352
- : `Continue without ${descriptor.displayName} for this run`;
442
+ const continueLabel = descriptor.fallbackDescription
443
+ ? `Continue with ${descriptor.fallbackDescription} for this run`
444
+ : `Continue without ${descriptor.displayName} for this run`;
353
445
  const snoozeLabel = `Snooze ${descriptor.snoozeDurationDays} days`;
354
446
  const neverLabel = `Never ask again`;
355
447
  const questionBody =
@@ -378,12 +470,11 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
378
470
  content: [
379
471
  {
380
472
  type: "text",
381
- text:
382
- `${buildSkippedTag({
383
- provider: parsed.provider,
384
- reason: "credential_not_provided",
385
- remediation: `run /connect ${descriptor.aliases[0] ?? descriptor.id} to unlock`,
386
- })}\n\nPrompt was cancelled.`,
473
+ text: `${buildSkippedTag({
474
+ provider: parsed.provider,
475
+ reason: "credential_not_provided",
476
+ remediation: `run /connect ${descriptor.aliases[0] ?? descriptor.id} to unlock`,
477
+ })}\n\nPrompt was cancelled.`,
387
478
  },
388
479
  ],
389
480
  };
@@ -421,12 +512,11 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
421
512
  content: [
422
513
  {
423
514
  type: "text",
424
- text:
425
- `${buildSkippedTag({
426
- provider: parsed.provider,
427
- reason: "credential_not_provided",
428
- remediation: `run /connect ${descriptor.aliases[0] ?? descriptor.id} to unlock`,
429
- })}\n\n${descriptor.displayName} connect was ${connectOutcomeDescription}.`,
515
+ text: `${buildSkippedTag({
516
+ provider: parsed.provider,
517
+ reason: "credential_not_provided",
518
+ remediation: `run /connect ${descriptor.aliases[0] ?? descriptor.id} to unlock`,
519
+ })}\n\n${descriptor.displayName} connect was ${connectOutcomeDescription}.`,
430
520
  },
431
521
  ],
432
522
  };
@@ -448,13 +538,12 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
448
538
  content: [
449
539
  {
450
540
  type: "text",
451
- text:
452
- `${buildSkippedTag({
453
- provider: parsed.provider,
454
- reason: "credential_not_provided",
455
- remediation,
456
- silenced,
457
- })}\n\n${descriptor.displayName} data was omitted per your choice.`,
541
+ text: `${buildSkippedTag({
542
+ provider: parsed.provider,
543
+ reason: "credential_not_provided",
544
+ remediation,
545
+ silenced,
546
+ })}\n\n${descriptor.displayName} data was omitted per your choice.`,
458
547
  },
459
548
  ],
460
549
  };
@@ -490,12 +579,16 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
490
579
  // Input handling — branches on OPENCANDLE_ROUTER_MODE.
491
580
  pi.on("input", async (event, ctx) => {
492
581
  if (event.source === "extension") return;
582
+ coordinator.clearTickerValidationCache();
493
583
 
494
584
  // Check for comprehensive analysis pattern — same in both modes.
495
585
  const analysis = isAnalysisRequest(event.text);
496
586
  if (analysis.match && analysis.symbol) {
497
- const definition = buildComprehensiveAnalysisDefinition(analysis.symbol, { debate: getConfig().debate });
587
+ const definition = buildComprehensiveAnalysisDefinition(analysis.symbol, {
588
+ debate: getConfig().debate,
589
+ });
498
590
  const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
591
+ if (prompt) markOriginalInput(event.text);
499
592
  return prompt ? { action: "transform", text: prompt } : { action: "handled" };
500
593
  }
501
594
 
@@ -516,49 +609,211 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
516
609
  const workflowPrefs = storage?.getWorkflowPreferences("global") ?? {};
517
610
 
518
611
  // Classify intent
519
- const classification = classifyWithLegacyRules(event.text);
612
+ let classification = classifyWithLegacyRules(event.text);
613
+ const ruleModeDisambiguation = disambiguateRulesModeSymbols(
614
+ event.text,
615
+ classification.entities.symbols,
616
+ );
617
+ appendSymbolDropEntries(ruleModeDisambiguation.dropped, "rules");
618
+ classification = {
619
+ ...classification,
620
+ entities: {
621
+ ...classification.entities,
622
+ symbols: ruleModeDisambiguation.kept,
623
+ },
624
+ };
625
+ if (
626
+ isComparePrompt(event.text) &&
627
+ ruleModeDisambiguation.dropped.length > 0 &&
628
+ classification.entities.symbols.length < 2
629
+ ) {
630
+ pi.appendEntry("opencandle-workflow-aborted", {
631
+ reason: "symbol-disambiguation-insufficient-symbols",
632
+ dropped: ruleModeDisambiguation.dropped,
633
+ validSymbols: classification.entities.symbols,
634
+ });
635
+ const base = coordinator.buildRouterContextBase(ctx.sessionManager);
636
+ const output: RouterOutput = {
637
+ routeKind: "clarification",
638
+ route: "fallback",
639
+ workflow: "compare_assets",
640
+ entities: classification.entities,
641
+ slots: {},
642
+ preference_updates: [],
643
+ missing_required: ["symbols"],
644
+ tool_bundles: ["clarification"],
645
+ diagnostics: ruleModeDisambiguation.dropped.map((drop) => ({
646
+ code: "symbol_dropped",
647
+ message: `${drop.token} dropped: ${drop.reason}`,
648
+ details: {
649
+ token: drop.token,
650
+ reason: drop.reason,
651
+ signalsChecked: drop.signalsChecked,
652
+ source: "rules",
653
+ },
654
+ })),
655
+ reasoning: "rules-mode acronym disambiguation left fewer than two symbols for comparison",
656
+ };
657
+ const resolvedTurnContext = buildResolvedTurnContext({ text: event.text, ...base }, output, {
658
+ availableToolNames: safeGetAllToolNames(),
659
+ planning: {
660
+ migrationStatuses: getConfig().planningMigrationStatuses,
661
+ },
662
+ });
663
+ coordinator.setPendingResolvedTurnContext({
664
+ ...resolvedTurnContext,
665
+ diagnostics: [
666
+ ...resolvedTurnContext.diagnostics,
667
+ {
668
+ code: "compare_workflow_aborted",
669
+ message:
670
+ "compare workflow needs at least two validated symbols after acronym disambiguation",
671
+ },
672
+ ],
673
+ });
674
+ coordinator.setPendingFallbackContext({
675
+ assumptionsBlock: [
676
+ "Assumptions Context:",
677
+ classification.entities.symbols.length > 0
678
+ ? ` valid symbols: ${classification.entities.symbols.join(", ")} (user)`
679
+ : " valid symbols: (none)",
680
+ ` dropped ambiguous ticker-like tokens: ${ruleModeDisambiguation.dropped.map((d) => d.token).join(", ")} (no positive ticker signal)`,
681
+ ].join("\n"),
682
+ missingRequired: ["symbols"],
683
+ extraContext:
684
+ "Dropped ambiguous ticker-like tokens: " +
685
+ `${ruleModeDisambiguation.dropped.map((d) => d.token).join(", ")}. ` +
686
+ "Ask the user which ticker symbols they want compared before calling comparison tools.",
687
+ });
688
+ applyRouteToolScope(resolvedTurnContext);
689
+ return undefined;
690
+ }
520
691
 
521
692
  if (classification.workflow === "portfolio_builder") {
522
693
  const resolution = resolvePortfolioSlots(classification.entities, workflowPrefs);
523
- coordinator.recordWorkflowRun("portfolio_builder", classification.entities, resolution.resolved, resolution.defaultsUsed);
524
- pi.appendEntry("opencandle-workflow", { workflow: "portfolio_builder", entities: classification.entities, resolved: resolution.resolved });
694
+ coordinator.recordWorkflowRun(
695
+ "portfolio_builder",
696
+ classification.entities,
697
+ resolution.resolved,
698
+ resolution.defaultsUsed,
699
+ );
700
+ pi.appendEntry("opencandle-workflow", {
701
+ workflow: "portfolio_builder",
702
+ entities: classification.entities,
703
+ resolved: resolution.resolved,
704
+ });
525
705
  const definition = buildPortfolioWorkflowDefinition(resolution);
526
706
  const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
707
+ if (prompt) markOriginalInput(event.text);
527
708
  return prompt ? { action: "transform", text: prompt } : { action: "handled" };
528
709
  }
529
710
 
530
711
  if (classification.workflow === "options_screener") {
531
712
  const resolution = resolveOptionsScreenerSlots(classification.entities, workflowPrefs);
532
713
  if (resolution.missingRequired.length === 0) {
533
- coordinator.recordWorkflowRun("options_screener", classification.entities, resolution.resolved, resolution.defaultsUsed);
534
- pi.appendEntry("opencandle-workflow", { workflow: "options_screener", entities: classification.entities, resolved: resolution.resolved });
714
+ coordinator.recordWorkflowRun(
715
+ "options_screener",
716
+ classification.entities,
717
+ resolution.resolved,
718
+ resolution.defaultsUsed,
719
+ );
720
+ pi.appendEntry("opencandle-workflow", {
721
+ workflow: "options_screener",
722
+ entities: classification.entities,
723
+ resolved: resolution.resolved,
724
+ });
535
725
  const definition = buildOptionsScreenerWorkflowDefinition(resolution);
536
726
  const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
727
+ if (prompt) markOriginalInput(event.text);
537
728
  return prompt ? { action: "transform", text: prompt } : { action: "handled" };
538
729
  }
539
730
  }
540
731
 
541
- if (classification.workflow === "compare_assets" && classification.entities.symbols.length >= 2) {
732
+ if (
733
+ classification.workflow === "compare_assets" &&
734
+ classification.entities.symbols.length >= 2
735
+ ) {
542
736
  const resolution: SlotResolution<CompareAssetsSlots> = {
543
737
  resolved: {
544
738
  symbols: classification.entities.symbols,
545
739
  metrics: classification.entities.compareMetrics,
546
740
  timeHorizon: classification.entities.timeHorizon,
741
+ budget: classification.entities.budget,
742
+ assetScope: classification.entities.assetScope,
547
743
  },
548
744
  sources: {
549
745
  symbols: "user",
550
746
  ...(classification.entities.timeHorizon ? { timeHorizon: "user" as const } : {}),
551
747
  ...(classification.entities.compareMetrics ? { metrics: "user" as const } : {}),
748
+ ...(classification.entities.budget !== undefined ? { budget: "user" as const } : {}),
749
+ ...(classification.entities.assetScope ? { assetScope: "user" as const } : {}),
552
750
  },
553
751
  defaultsUsed: [],
554
752
  missingRequired: [],
555
753
  };
556
- coordinator.recordWorkflowRun("compare_assets", classification.entities, resolution.resolved, resolution.defaultsUsed);
557
- pi.appendEntry("opencandle-workflow", { workflow: "compare_assets", symbols: classification.entities.symbols });
558
- const definition = buildCompareAssetsWorkflowDefinition(resolution);
754
+ coordinator.recordWorkflowRun(
755
+ "compare_assets",
756
+ classification.entities,
757
+ resolution.resolved,
758
+ resolution.defaultsUsed,
759
+ );
760
+ pi.appendEntry("opencandle-workflow", {
761
+ workflow: "compare_assets",
762
+ symbols: classification.entities.symbols,
763
+ });
764
+ const preflight = await preflightCompareResolution(resolution);
765
+ if (!preflight) {
766
+ coordinator.recordWorkflowRun(
767
+ "fallback",
768
+ classification.entities,
769
+ resolution.resolved,
770
+ [],
771
+ "clarification",
772
+ );
773
+ coordinator.setPendingFallbackContext({
774
+ assumptionsBlock: [
775
+ "Assumptions Context:",
776
+ ` original symbols: ${classification.entities.symbols.join(", ")} (user)`,
777
+ ].join("\n"),
778
+ missingRequired: ["symbols"],
779
+ extraContext:
780
+ "Compare workflow aborted because ticker preflight left fewer than two valid symbols. Ask the user to clarify the intended tickers before calling comparison tools.",
781
+ });
782
+ return undefined;
783
+ }
784
+ const definition = buildCompareAssetsWorkflowDefinition(preflight.resolution);
785
+ applyPreflightAnnotation(definition, preflight.dropped);
559
786
  const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
787
+ if (prompt) markOriginalInput(event.text);
560
788
  return prompt ? { action: "transform", text: prompt } : { action: "handled" };
561
789
  }
790
+
791
+ // Rules-mode finance fallback: no workflow dispatched, but the turn is
792
+ // finance-shaped (classified finance intent, extracted symbols, or finance
793
+ // vocabulary). Record the fallback turn and stash a fallback context so
794
+ // the system prompt carries saved market state for this turn; non-finance
795
+ // prompts stay untouched.
796
+ const isFinanceFallback =
797
+ classification.workflow !== "unclassified" ||
798
+ classification.entities.symbols.length > 0 ||
799
+ hasFinanceSignals(event.text);
800
+ if (isFinanceFallback) {
801
+ coordinator.recordWorkflowRun("fallback", classification.entities, {}, [], "agent_task");
802
+ coordinator.setPendingFallbackContext({
803
+ assumptionsBlock: "",
804
+ missingRequired: [],
805
+ extraContext:
806
+ classification.entities.symbols.length > 0
807
+ ? `Rules-router extracted symbols: ${classification.entities.symbols.join(", ")}.`
808
+ : undefined,
809
+ });
810
+ pi.appendEntry("opencandle-fallback-context", {
811
+ mode: "rules",
812
+ classifiedWorkflow: classification.workflow,
813
+ symbols: classification.entities.symbols,
814
+ });
815
+ }
816
+ return undefined;
562
817
  });
563
818
 
564
819
  /**
@@ -573,8 +828,9 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
573
828
  ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1],
574
829
  ): Promise<{ action: "transform"; text: string } | false> {
575
830
  const storage = coordinator.getStorage();
576
- const { profileSnapshot, recentWorkflowRuns, priorTurns } =
577
- coordinator.buildRouterContextBase(ctx.sessionManager);
831
+ const { profileSnapshot, recentWorkflowRuns, priorTurns } = coordinator.buildRouterContextBase(
832
+ ctx.sessionManager,
833
+ );
578
834
  // priorTurns is not scrubbed for /forget — tracked in proposal.md follow-ups.
579
835
  const input: RouterInputContext = {
580
836
  text,
@@ -628,6 +884,7 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
628
884
  });
629
885
 
630
886
  pi.appendEntry("opencandle-router", { output });
887
+ appendRouterSymbolDropEntries(output);
631
888
  pi.appendEntry("opencandle-route-context", resolvedTurnContext);
632
889
  coordinator.setPendingResolvedTurnContext(resolvedTurnContext);
633
890
  applyRouteToolScope(resolvedTurnContext);
@@ -653,7 +910,7 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
653
910
 
654
911
  // Workflow dispatch for recognised workflows.
655
912
  if (output.routeKind === "workflow_dispatch" && output.workflow) {
656
- return dispatchRouterWorkflow(output, ctx);
913
+ return dispatchRouterWorkflow(output, ctx, text);
657
914
  }
658
915
 
659
916
  if (output.routeKind === "pass_through") {
@@ -674,18 +931,20 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
674
931
  coordinator.setPendingFallbackContext({
675
932
  assumptionsBlock,
676
933
  missingRequired: output.missing_required,
677
- extraContext: output.entities.symbols.length > 0
678
- ? `Router-extracted symbols: ${output.entities.symbols.join(", ")}.`
679
- + ` Route kind: ${output.routeKind}. Tool bundles: ${output.tool_bundles.join(", ") || "(none)"}.`
680
- : undefined,
934
+ extraContext:
935
+ output.entities.symbols.length > 0
936
+ ? `Router-extracted symbols: ${output.entities.symbols.join(", ")}.` +
937
+ ` Route kind: ${output.routeKind}. Tool bundles: ${output.tool_bundles.join(", ") || "(none)"}.`
938
+ : undefined,
681
939
  });
682
940
  return false;
683
941
  }
684
942
 
685
- function dispatchRouterWorkflow(
943
+ async function dispatchRouterWorkflow(
686
944
  output: RouterOutput,
687
945
  ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1],
688
- ): { action: "transform"; text: string } | false {
946
+ originalText: string,
947
+ ): Promise<{ action: "transform"; text: string } | false> {
689
948
  const workflow = output.workflow!;
690
949
  const storage = coordinator.getStorage();
691
950
  const workflowPrefs = storage?.getWorkflowPreferences("global") ?? {};
@@ -710,6 +969,7 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
710
969
  });
711
970
  const definition = buildPortfolioWorkflowDefinition(resolution);
712
971
  const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
972
+ if (prompt) markOriginalInput(originalText);
713
973
  return prompt ? { action: "transform", text: prompt } : false;
714
974
  }
715
975
  if (workflow === "options_screener") {
@@ -734,6 +994,7 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
734
994
  });
735
995
  const definition = buildOptionsScreenerWorkflowDefinition(resolution);
736
996
  const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
997
+ if (prompt) markOriginalInput(originalText);
737
998
  return prompt ? { action: "transform", text: prompt } : false;
738
999
  }
739
1000
  // Missing required symbol — treat as fallback with ask_user directive.
@@ -744,11 +1005,17 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
744
1005
  symbols: entities.symbols,
745
1006
  metrics: entities.compareMetrics,
746
1007
  timeHorizon: entities.timeHorizon,
1008
+ budget: entities.budget,
1009
+ assetScope: entities.assetScope,
747
1010
  },
748
1011
  sources: {
749
1012
  symbols: sourceForRouterSlot(output, "symbols", "user"),
750
1013
  ...(entities.timeHorizon ? { timeHorizon: "user" as const } : {}),
751
1014
  ...(entities.compareMetrics ? { metrics: "user" as const } : {}),
1015
+ ...(entities.budget !== undefined
1016
+ ? { budget: sourceForRouterSlot(output, "budget", "user") }
1017
+ : {}),
1018
+ ...(entities.assetScope ? { assetScope: "user" as const } : {}),
752
1019
  },
753
1020
  defaultsUsed: [],
754
1021
  missingRequired: [],
@@ -764,8 +1031,28 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
764
1031
  workflow: "compare_assets",
765
1032
  symbols: entities.symbols,
766
1033
  });
767
- const definition = buildCompareAssetsWorkflowDefinition(resolution);
1034
+ const preflight = await preflightCompareResolution(resolution);
1035
+ if (!preflight) {
1036
+ coordinator.recordWorkflowRun(
1037
+ "fallback",
1038
+ output.entities,
1039
+ Object.fromEntries(Object.entries(output.slots).map(([k, v]) => [k, v.value])),
1040
+ [],
1041
+ output.routeKind,
1042
+ );
1043
+ coordinator.setPendingResolvedTurnContext(null);
1044
+ coordinator.setPendingFallbackContext({
1045
+ assumptionsBlock: buildAssumptionsBlockFromRouter(output.slots),
1046
+ missingRequired: ["symbols"],
1047
+ extraContext:
1048
+ "Compare workflow aborted because ticker preflight left fewer than two valid symbols. Ask the user to clarify the intended tickers.",
1049
+ });
1050
+ return false;
1051
+ }
1052
+ const definition = buildCompareAssetsWorkflowDefinition(preflight.resolution);
1053
+ applyPreflightAnnotation(definition, preflight.dropped);
768
1054
  const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
1055
+ if (prompt) markOriginalInput(originalText);
769
1056
  return prompt ? { action: "transform", text: prompt } : false;
770
1057
  }
771
1058
 
@@ -788,6 +1075,112 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
788
1075
  return false;
789
1076
  }
790
1077
 
1078
+ function appendRouterSymbolDropEntries(output: RouterOutput): void {
1079
+ for (const diagnostic of output.diagnostics) {
1080
+ if (diagnostic.code !== "symbol_dropped") continue;
1081
+ const details = diagnostic.details ?? {};
1082
+ appendSymbolDropEntries(
1083
+ [
1084
+ {
1085
+ token: String(details.token ?? ""),
1086
+ reason: String(details.reason ?? ""),
1087
+ signalsChecked: Array.isArray(details.signalsChecked)
1088
+ ? details.signalsChecked.map(String)
1089
+ : [],
1090
+ },
1091
+ ],
1092
+ String(details.source ?? "llm"),
1093
+ );
1094
+ }
1095
+ }
1096
+
1097
+ function appendSymbolDropEntries(
1098
+ dropped: Array<{ token: string; reason: string; signalsChecked: string[] }>,
1099
+ source: string,
1100
+ ): void {
1101
+ for (const drop of dropped) {
1102
+ pi.appendEntry("opencandle-symbol-dropped", {
1103
+ token: drop.token,
1104
+ reason: drop.reason,
1105
+ signalsChecked: drop.signalsChecked,
1106
+ source,
1107
+ });
1108
+ }
1109
+ }
1110
+
1111
+ function disambiguateRulesModeSymbols(
1112
+ text: string,
1113
+ extractedSymbols: string[],
1114
+ ): {
1115
+ kept: string[];
1116
+ dropped: Array<{ token: string; reason: string; signalsChecked: string[] }>;
1117
+ } {
1118
+ const candidates = mergeSymbols(extractedSymbols, rawTickerLikeTokens(text));
1119
+ const disambiguated = disambiguateSymbols(candidates, text);
1120
+ return {
1121
+ kept: disambiguated.kept.filter((symbol) => extractedSymbols.includes(symbol)),
1122
+ dropped: disambiguated.dropped,
1123
+ };
1124
+ }
1125
+
1126
+ function rawTickerLikeTokens(text: string): string[] {
1127
+ const tokens: string[] = [];
1128
+ for (const match of text.matchAll(/\$?([A-Za-z]{1,5})\b/g)) {
1129
+ const raw = match[1];
1130
+ if (raw !== raw.toUpperCase()) continue;
1131
+ const token = raw.toUpperCase();
1132
+ if (!tokens.includes(token)) tokens.push(token);
1133
+ }
1134
+ return tokens;
1135
+ }
1136
+
1137
+ function isComparePrompt(text: string): boolean {
1138
+ return /\b(?:compare|vs\.?|versus|which\s+is\s+better)\b/i.test(text);
1139
+ }
1140
+
1141
+ async function preflightCompareResolution(
1142
+ resolution: SlotResolution<CompareAssetsSlots>,
1143
+ ): Promise<{
1144
+ resolution: SlotResolution<CompareAssetsSlots>;
1145
+ dropped: Array<{ symbol: string; reason: string }>;
1146
+ } | null> {
1147
+ const result = await preflightSymbols(resolution.resolved.symbols, {
1148
+ cache: coordinator.getTickerValidationCache(),
1149
+ search: options?.symbolSearch,
1150
+ });
1151
+ for (const drop of result.dropped) {
1152
+ pi.appendEntry("opencandle-symbol-preflight-dropped", drop);
1153
+ }
1154
+ if (result.valid.length < 2) {
1155
+ pi.appendEntry("opencandle-workflow-aborted", {
1156
+ reason: "preflight-insufficient-symbols",
1157
+ dropped: result.dropped,
1158
+ });
1159
+ return null;
1160
+ }
1161
+ return {
1162
+ resolution: {
1163
+ ...resolution,
1164
+ resolved: {
1165
+ ...resolution.resolved,
1166
+ symbols: result.valid,
1167
+ },
1168
+ },
1169
+ dropped: result.dropped,
1170
+ };
1171
+ }
1172
+
1173
+ function applyPreflightAnnotation(
1174
+ definition: ReturnType<typeof buildCompareAssetsWorkflowDefinition>,
1175
+ dropped: Array<{ symbol: string; reason: string }>,
1176
+ ): void {
1177
+ if (dropped.length === 0 || definition.steps.length === 0) return;
1178
+ definition.steps[0] = {
1179
+ ...definition.steps[0],
1180
+ prompt: `${formatPreflightDropAnnotation(dropped)}\n\n${definition.steps[0].prompt}`,
1181
+ };
1182
+ }
1183
+
791
1184
  function mergeRouterSlotsIntoEntities(output: RouterOutput): ExtractedEntities {
792
1185
  const entities: ExtractedEntities = {
793
1186
  ...output.entities,
@@ -798,7 +1191,10 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
798
1191
  entities.budget = output.slots.budget.value;
799
1192
  }
800
1193
 
801
- const slotSymbols = symbolsFromRouterSlots(output);
1194
+ const droppedSymbols = droppedSymbolsFromDiagnostics(output);
1195
+ const slotSymbols = symbolsFromRouterSlots(output).filter(
1196
+ (symbol) => !droppedSymbols.has(symbol),
1197
+ );
802
1198
  if (slotSymbols.length > 0 && slotSymbols.length > entities.symbols.length) {
803
1199
  entities.symbols = mergeSymbols(slotSymbols, entities.symbols);
804
1200
  }
@@ -806,6 +1202,18 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
806
1202
  return entities;
807
1203
  }
808
1204
 
1205
+ function droppedSymbolsFromDiagnostics(output: RouterOutput): Set<string> {
1206
+ const dropped = new Set<string>();
1207
+ for (const diagnostic of output.diagnostics) {
1208
+ if (diagnostic.code !== "symbol_dropped") continue;
1209
+ const token = diagnostic.details?.token;
1210
+ if (typeof token === "string" && token.trim() !== "") {
1211
+ dropped.add(token.toUpperCase());
1212
+ }
1213
+ }
1214
+ return dropped;
1215
+ }
1216
+
809
1217
  function withRouterSlotSources<T extends object>(
810
1218
  resolution: SlotResolution<T>,
811
1219
  output: RouterOutput,
@@ -946,10 +1354,46 @@ export default function openCandleExtension(pi: ExtensionAPI, options?: OpenCand
946
1354
  return {
947
1355
  systemPrompt: coordinator.buildSystemPrompt(
948
1356
  event.systemPrompt,
949
- undefined,
1357
+ coordinator.getActiveWorkflowType(),
950
1358
  fallbackContext,
951
1359
  resolvedTurnContext,
952
1360
  ),
953
1361
  };
954
1362
  });
955
1363
  }
1364
+
1365
+ /** Concatenate text from a Pi message `content` (plain string or block array). */
1366
+ function extractMessageText(content: unknown): string {
1367
+ if (typeof content === "string") return content;
1368
+ if (!Array.isArray(content)) return "";
1369
+ let text = "";
1370
+ for (const block of content) {
1371
+ if (
1372
+ block &&
1373
+ typeof block === "object" &&
1374
+ (block as { type?: unknown }).type === "text" &&
1375
+ typeof (block as { text?: unknown }).text === "string"
1376
+ ) {
1377
+ text += (block as { text: string }).text;
1378
+ }
1379
+ }
1380
+ return text;
1381
+ }
1382
+
1383
+ /**
1384
+ * True when the session name is still an auto-set placeholder that the LLM
1385
+ * title may replace: unset, exactly one of the candidate user texts, or the
1386
+ * GUI server's truncated "first 77 chars + ..." form of one of them.
1387
+ */
1388
+ function isPlaceholderSessionName(name: string | undefined, candidates: string[]): boolean {
1389
+ if (name === undefined || name.trim().length === 0) return true;
1390
+ const trimmed = name.trim();
1391
+ for (const candidate of candidates) {
1392
+ const candidateTrimmed = candidate.trim();
1393
+ if (trimmed === candidateTrimmed) return true;
1394
+ if (trimmed.endsWith("...") && candidateTrimmed.startsWith(trimmed.slice(0, -3))) {
1395
+ return true;
1396
+ }
1397
+ }
1398
+ return false;
1399
+ }