dexterm-cli 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/dist/packages/agents/src/audit.js +122 -0
- package/dist/packages/agents/src/bus.js +379 -0
- package/dist/packages/agents/src/executor.js +177 -0
- package/dist/packages/agents/src/gate.js +176 -0
- package/dist/packages/agents/src/identity.js +179 -0
- package/dist/packages/agents/src/index.js +15 -0
- package/dist/packages/agents/src/proposals.js +153 -0
- package/dist/packages/agents/src/schema.js +87 -0
- package/dist/packages/agents/src/sdk.js +140 -0
- package/dist/packages/agents/src/tiers.js +114 -0
- package/dist/packages/agents/src/tools.js +69 -0
- package/dist/packages/agents/src/ws.js +158 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
- package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +67 -0
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +64 -0
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
- package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
- package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
- package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
- package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
- package/dist/packages/cli/bin/dexterm.mjs +11 -0
- package/dist/packages/cli/src/agent-cmd.js +303 -0
- package/dist/packages/cli/src/agent-roster.js +26 -0
- package/dist/packages/cli/src/analyst/backend.js +526 -0
- package/dist/packages/cli/src/analyst/results.js +38 -0
- package/dist/packages/cli/src/analyst/session.js +319 -0
- package/dist/packages/cli/src/audit.js +51 -0
- package/dist/packages/cli/src/backtest-cmd.js +149 -0
- package/dist/packages/cli/src/boot-state.js +62 -0
- package/dist/packages/cli/src/custom.js +261 -0
- package/dist/packages/cli/src/display.js +191 -0
- package/dist/packages/cli/src/doctor.js +361 -0
- package/dist/packages/cli/src/feed.js +634 -0
- package/dist/packages/cli/src/fixtures.js +275 -0
- package/dist/packages/cli/src/follow-cmd.js +221 -0
- package/dist/packages/cli/src/images-config.js +59 -0
- package/dist/packages/cli/src/index.js +31 -0
- package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
- package/dist/packages/cli/src/larp-record.js +21 -0
- package/dist/packages/cli/src/live/copy.js +80 -0
- package/dist/packages/cli/src/live/flow.js +233 -0
- package/dist/packages/cli/src/live/index.js +509 -0
- package/dist/packages/cli/src/live/intel.js +227 -0
- package/dist/packages/cli/src/live/newpairs.js +334 -0
- package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
- package/dist/packages/cli/src/live/screens/token-live.js +459 -0
- package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
- package/dist/packages/cli/src/live/token.js +585 -0
- package/dist/packages/cli/src/live/trade.js +929 -0
- package/dist/packages/cli/src/live/watchlist.js +178 -0
- package/dist/packages/cli/src/main.js +5583 -0
- package/dist/packages/cli/src/narrative-cmd.js +76 -0
- package/dist/packages/cli/src/pack-cmd.js +323 -0
- package/dist/packages/cli/src/plan-cmd.js +278 -0
- package/dist/packages/cli/src/popup.js +810 -0
- package/dist/packages/cli/src/rules-cmd.js +131 -0
- package/dist/packages/cli/src/rules.js +339 -0
- package/dist/packages/cli/src/scan/command.js +57 -0
- package/dist/packages/cli/src/scan/engine.js +419 -0
- package/dist/packages/cli/src/scan/observe.js +64 -0
- package/dist/packages/cli/src/scan/ui.js +98 -0
- package/dist/packages/cli/src/screener/filter.js +241 -0
- package/dist/packages/cli/src/screener/presets.js +80 -0
- package/dist/packages/cli/src/screener/rows.js +146 -0
- package/dist/packages/cli/src/screener/rug.js +50 -0
- package/dist/packages/cli/src/screener/sort.js +52 -0
- package/dist/packages/cli/src/screener/view.js +183 -0
- package/dist/packages/cli/src/screens/analyst-session.js +343 -0
- package/dist/packages/cli/src/screens/analyst.js +326 -0
- package/dist/packages/cli/src/screens/boot.js +74 -0
- package/dist/packages/cli/src/screens/bubbles.js +255 -0
- package/dist/packages/cli/src/screens/coin.js +33 -0
- package/dist/packages/cli/src/screens/follows.js +201 -0
- package/dist/packages/cli/src/screens/kol.js +90 -0
- package/dist/packages/cli/src/screens/narrative.js +90 -0
- package/dist/packages/cli/src/screens/newpairs.js +150 -0
- package/dist/packages/cli/src/screens/overlay.js +257 -0
- package/dist/packages/cli/src/screens/plans.js +126 -0
- package/dist/packages/cli/src/screens/positions.js +176 -0
- package/dist/packages/cli/src/screens/rules.js +170 -0
- package/dist/packages/cli/src/screens/screener.js +459 -0
- package/dist/packages/cli/src/screens/social-panel.js +107 -0
- package/dist/packages/cli/src/screens/ticket.js +342 -0
- package/dist/packages/cli/src/screens/token.js +312 -0
- package/dist/packages/cli/src/screens/trending.js +181 -0
- package/dist/packages/cli/src/screens/watchlist.js +104 -0
- package/dist/packages/cli/src/social/demo.js +96 -0
- package/dist/packages/cli/src/social/view.js +250 -0
- package/dist/packages/cli/src/state.js +463 -0
- package/dist/packages/cli/src/theme-cmd.js +126 -0
- package/dist/packages/cli/src/trade.js +319 -0
- package/dist/packages/cli/src/voice-hud.js +42 -0
- package/dist/packages/cli/src/wallet-cmd.js +440 -0
- package/dist/packages/cli/src/wallet-command.js +31 -0
- package/dist/packages/cli/src/widgets.js +208 -0
- package/dist/packages/cli/src/workspace-cmd.js +68 -0
- package/dist/packages/cli/src/x-cmd.js +154 -0
- package/dist/packages/custom/src/config.js +213 -0
- package/dist/packages/custom/src/index.js +10 -0
- package/dist/packages/custom/src/keymap.js +107 -0
- package/dist/packages/custom/src/layout.js +235 -0
- package/dist/packages/custom/src/packs.js +633 -0
- package/dist/packages/custom/src/rules.js +704 -0
- package/dist/packages/custom/src/schema.js +515 -0
- package/dist/packages/custom/src/themes.js +176 -0
- package/dist/packages/custom/src/widget-html.js +222 -0
- package/dist/packages/custom/src/workspace.js +141 -0
- package/dist/packages/data/src/analyst-actions.js +133 -0
- package/dist/packages/data/src/budget.js +88 -0
- package/dist/packages/data/src/cache.js +142 -0
- package/dist/packages/data/src/candles.js +168 -0
- package/dist/packages/data/src/clock.js +64 -0
- package/dist/packages/data/src/credits.js +137 -0
- package/dist/packages/data/src/detail.js +97 -0
- package/dist/packages/data/src/fast-lane.js +321 -0
- package/dist/packages/data/src/fixtures.js +135 -0
- package/dist/packages/data/src/health.js +245 -0
- package/dist/packages/data/src/http.js +277 -0
- package/dist/packages/data/src/images/cache.js +141 -0
- package/dist/packages/data/src/images/decode-worker.js +28 -0
- package/dist/packages/data/src/images/decode.js +81 -0
- package/dist/packages/data/src/images/fetch.js +162 -0
- package/dist/packages/data/src/images/gif.js +214 -0
- package/dist/packages/data/src/images/index.js +10 -0
- package/dist/packages/data/src/images/jpeg.js +544 -0
- package/dist/packages/data/src/images/png.js +310 -0
- package/dist/packages/data/src/images/service.js +334 -0
- package/dist/packages/data/src/images/types.js +27 -0
- package/dist/packages/data/src/index.js +208 -0
- package/dist/packages/data/src/intel/helius-history.js +59 -0
- package/dist/packages/data/src/intel/holder-class.js +125 -0
- package/dist/packages/data/src/intel/rules.js +421 -0
- package/dist/packages/data/src/intel/scan.js +170 -0
- package/dist/packages/data/src/larp/evidence.js +288 -0
- package/dist/packages/data/src/larp/links.js +137 -0
- package/dist/packages/data/src/larp/robots.js +90 -0
- package/dist/packages/data/src/larp/safe-fetch.js +389 -0
- package/dist/packages/data/src/limiter.js +341 -0
- package/dist/packages/data/src/model.js +12 -0
- package/dist/packages/data/src/paper.js +331 -0
- package/dist/packages/data/src/rpc-config.js +126 -0
- package/dist/packages/data/src/rpc-pool.js +166 -0
- package/dist/packages/data/src/social/chain.js +83 -0
- package/dist/packages/data/src/social/dedupe.js +205 -0
- package/dist/packages/data/src/social/feed.js +462 -0
- package/dist/packages/data/src/social/firewall.js +110 -0
- package/dist/packages/data/src/social/index.js +11 -0
- package/dist/packages/data/src/social/kol.js +269 -0
- package/dist/packages/data/src/social/model.js +11 -0
- package/dist/packages/data/src/social/rank.js +184 -0
- package/dist/packages/data/src/social/rss.js +113 -0
- package/dist/packages/data/src/social/summary.js +104 -0
- package/dist/packages/data/src/social/venues.js +186 -0
- package/dist/packages/data/src/sources/dexscreener.js +170 -0
- package/dist/packages/data/src/sources/geckoterminal.js +133 -0
- package/dist/packages/data/src/sources/goplus.js +161 -0
- package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
- package/dist/packages/data/src/sources/jupiter.js +198 -0
- package/dist/packages/data/src/sources/pda.js +113 -0
- package/dist/packages/data/src/sources/pump-curve.js +58 -0
- package/dist/packages/data/src/sources/pumpportal.js +247 -0
- package/dist/packages/data/src/sources/rugcheck.js +71 -0
- package/dist/packages/data/src/sources/solana-rpc.js +299 -0
- package/dist/packages/data/src/sources/solana-ws.js +287 -0
- package/dist/packages/data/src/store.js +289 -0
- package/dist/packages/data/src/trade/bridge.js +464 -0
- package/dist/packages/data/src/trade/caps.js +323 -0
- package/dist/packages/data/src/trade/copy/auto.js +385 -0
- package/dist/packages/data/src/trade/copy/follows.js +103 -0
- package/dist/packages/data/src/trade/copy/index.js +9 -0
- package/dist/packages/data/src/trade/copy/mirror.js +91 -0
- package/dist/packages/data/src/trade/copy/stats.js +120 -0
- package/dist/packages/data/src/trade/copy/tracker.js +158 -0
- package/dist/packages/data/src/trade/copy/trades.js +87 -0
- package/dist/packages/data/src/trade/copy/types.js +6 -0
- package/dist/packages/data/src/trade/executor.js +933 -0
- package/dist/packages/data/src/trade/kill-switch.js +47 -0
- package/dist/packages/data/src/trade/link-store.js +52 -0
- package/dist/packages/data/src/trade/plans/confirm.js +157 -0
- package/dist/packages/data/src/trade/plans/engine.js +784 -0
- package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
- package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
- package/dist/packages/data/src/trade/plans/executor.js +11 -0
- package/dist/packages/data/src/trade/plans/index.js +12 -0
- package/dist/packages/data/src/trade/plans/journal.js +77 -0
- package/dist/packages/data/src/trade/plans/mandate.js +129 -0
- package/dist/packages/data/src/trade/plans/plan.js +179 -0
- package/dist/packages/data/src/trade/plans/service.js +168 -0
- package/dist/packages/data/src/trade/plans/types.js +6 -0
- package/dist/packages/data/src/trade/rpc-exec.js +229 -0
- package/dist/packages/data/src/trade/signer/client.js +197 -0
- package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
- package/dist/packages/data/src/trade/signer/host.js +97 -0
- package/dist/packages/data/src/trade/signer/main.js +121 -0
- package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
- package/dist/packages/data/src/trade/signer/protocol.js +92 -0
- package/dist/packages/data/src/trade/signer/service.js +400 -0
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
- package/dist/packages/data/src/trade/sol-usd.js +86 -0
- package/dist/packages/data/src/trade/swap-build.js +108 -0
- package/dist/packages/data/src/trade/token-delta.js +201 -0
- package/dist/packages/data/src/trade/tx-decode.js +632 -0
- package/dist/packages/data/src/util.js +24 -0
- package/dist/packages/data/src/watchlist.js +176 -0
- package/dist/packages/data/src/x/api.js +256 -0
- package/dist/packages/data/src/x/budget.js +66 -0
- package/dist/packages/data/src/x/config.js +49 -0
- package/dist/packages/data/src/x/index.js +10 -0
- package/dist/packages/data/src/x/mock-server.js +217 -0
- package/dist/packages/data/src/x/oauth.js +242 -0
- package/dist/packages/data/src/x/pkce.js +21 -0
- package/dist/packages/data/src/x/source.js +285 -0
- package/dist/packages/data/src/x/token-store.js +134 -0
- package/dist/packages/data/src/x/transport.js +71 -0
- package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
- package/dist/packages/mcp/assets/cases.json +1194 -0
- package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
- package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
- package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
- package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
- package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
- package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
- package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
- package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
- package/dist/packages/mcp/src/dossier.js +78 -0
- package/dist/packages/mcp/src/firewall.js +21 -0
- package/dist/packages/mcp/src/index.js +22 -0
- package/dist/packages/mcp/src/server.js +849 -0
- package/dist/packages/mcp/src/user-content.js +434 -0
- package/dist/packages/packs/src/backtest.js +183 -0
- package/dist/packages/packs/src/digest.js +95 -0
- package/dist/packages/packs/src/firewall.js +57 -0
- package/dist/packages/packs/src/index.js +19 -0
- package/dist/packages/packs/src/larp-eval.js +100 -0
- package/dist/packages/packs/src/narrative.js +309 -0
- package/dist/packages/packs/src/reason.js +135 -0
- package/dist/packages/packs/src/rug-model.js +137 -0
- package/dist/packages/packs/src/schema.js +54 -0
- package/dist/packages/packs/src/token-dossier.js +174 -0
- package/dist/packages/packs/src/verdicts.js +17 -0
- package/dist/packages/packs/src/wallet-dossier.js +105 -0
- package/dist/packages/popup/src/config.js +71 -0
- package/dist/packages/popup/src/dock.js +271 -0
- package/dist/packages/popup/src/host.js +210 -0
- package/dist/packages/popup/src/index.js +12 -0
- package/dist/packages/popup/src/loopback.js +53 -0
- package/dist/packages/popup/src/open.js +183 -0
- package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
- package/dist/packages/popup/src/pages/bubbles.css +23 -0
- package/dist/packages/popup/src/pages/bubbles.html +36 -0
- package/dist/packages/popup/src/pages/bubbles.js +214 -0
- package/dist/packages/popup/src/pages/chart-lib.js +262 -0
- package/dist/packages/popup/src/pages/chart.css +50 -0
- package/dist/packages/popup/src/pages/chart.html +56 -0
- package/dist/packages/popup/src/pages/chart.js +554 -0
- package/dist/packages/popup/src/pages/common.css +34 -0
- package/dist/packages/popup/src/pages/common.js +187 -0
- package/dist/packages/popup/src/pages/feeds.html +20 -0
- package/dist/packages/popup/src/pages/feeds.js +77 -0
- package/dist/packages/popup/src/pages/narrative.html +20 -0
- package/dist/packages/popup/src/pages/narrative.js +61 -0
- package/dist/packages/popup/src/pages/plans.css +30 -0
- package/dist/packages/popup/src/pages/plans.html +26 -0
- package/dist/packages/popup/src/pages/plans.js +82 -0
- package/dist/packages/popup/src/pages/roster.css +41 -0
- package/dist/packages/popup/src/pages/roster.html +25 -0
- package/dist/packages/popup/src/pages/roster.js +75 -0
- package/dist/packages/popup/src/pages/token.html +20 -0
- package/dist/packages/popup/src/pages/token.js +112 -0
- package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
- package/dist/packages/popup/src/pages/widget.css +5 -0
- package/dist/packages/popup/src/pages/widget.html +21 -0
- package/dist/packages/popup/src/pages/widget.js +85 -0
- package/dist/packages/popup/src/protocol.js +31 -0
- package/dist/packages/popup/src/selftest.js +156 -0
- package/dist/packages/popup/src/server.js +582 -0
- package/dist/packages/popup/src/sinkproxy.js +167 -0
- package/dist/packages/popup/src/widget-html.js +231 -0
- package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
- package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
- package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
- package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
- package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
- package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
- package/dist/packages/recorder/src/cli.js +113 -0
- package/dist/packages/recorder/src/export.js +71 -0
- package/dist/packages/recorder/src/index.js +20 -0
- package/dist/packages/recorder/src/labels.js +151 -0
- package/dist/packages/recorder/src/open.js +29 -0
- package/dist/packages/recorder/src/recorder.js +553 -0
- package/dist/packages/recorder/src/schedule.js +43 -0
- package/dist/packages/recorder/src/schema.js +135 -0
- package/dist/packages/recorder/src/snapshot.js +191 -0
- package/dist/packages/recorder/src/sweep.js +52 -0
- package/dist/packages/recorder/src/verdicts.js +52 -0
- package/dist/packages/tui/src/bubbles.js +415 -0
- package/dist/packages/tui/src/canvas.js +370 -0
- package/dist/packages/tui/src/chrome.js +85 -0
- package/dist/packages/tui/src/color.js +133 -0
- package/dist/packages/tui/src/flush.js +62 -0
- package/dist/packages/tui/src/fmt.js +188 -0
- package/dist/packages/tui/src/fx-trade.js +278 -0
- package/dist/packages/tui/src/fx.js +527 -0
- package/dist/packages/tui/src/images/blocks.js +141 -0
- package/dist/packages/tui/src/images/coin.js +22 -0
- package/dist/packages/tui/src/images/detect.js +140 -0
- package/dist/packages/tui/src/images/index.js +7 -0
- package/dist/packages/tui/src/images/layer.js +146 -0
- package/dist/packages/tui/src/images/pixels.js +157 -0
- package/dist/packages/tui/src/images/protocols.js +87 -0
- package/dist/packages/tui/src/index.js +15 -0
- package/dist/packages/tui/src/keymap.js +355 -0
- package/dist/packages/tui/src/layout.js +11 -0
- package/dist/packages/tui/src/sound.js +177 -0
- package/dist/packages/tui/src/theme.js +182 -0
- package/dist/packages/tui/src/widgets.js +598 -0
- package/dist/packages/voice/src/controller.js +395 -0
- package/dist/packages/voice/src/doctor.js +63 -0
- package/dist/packages/voice/src/engines.js +44 -0
- package/dist/packages/voice/src/index.js +16 -0
- package/dist/packages/voice/src/router.js +502 -0
- package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
- package/dist/packages/voice/src/stt/apple.js +179 -0
- package/dist/packages/voice/src/stt/fake.js +34 -0
- package/dist/packages/voice/src/stt/whisper.js +102 -0
- package/dist/packages/voice/src/summary.js +44 -0
- package/dist/packages/voice/src/tts/fake.js +30 -0
- package/dist/packages/voice/src/tts/say.js +96 -0
- package/dist/packages/voice/src/types.js +3 -0
- package/package.json +42 -4
- package/README.md +0 -3
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
// dexterm history recorder (lane R-01, AI-SUITE-DESIGN §4.1). One class, four jobs:
|
|
2
|
+
// observe rows the data layer already normalised (Store listener) -> token_snapshots on change,
|
|
3
|
+
// feed_members on entry / every few minutes. Zero requests.
|
|
4
|
+
// sweep re-observe mints that left the screen at the 1 h / 2 h / 6 h / 24 h / 72 h horizons: one
|
|
5
|
+
// DexScreener token batch (30 mints) per call, queued at the limiter's LOWEST priority
|
|
6
|
+
// ('low': never takes the reserve, always behind foreground work), capped per sweep and per
|
|
7
|
+
// day, skipped while the bucket is paused after a 429. The only requests this package makes.
|
|
8
|
+
// label code-only outcome labels per (t0 anchor, horizon) from the recorded numbers (labels.ts).
|
|
9
|
+
// prune retention: raw JSON 7 d (focus rows only), one row per mint per minute after 24 h,
|
|
10
|
+
// columns 90 d, outcomes/verdicts forever.
|
|
11
|
+
// Everything is synchronous SQLite on the app's own DB file (node:sqlite, WAL), batched in transactions,
|
|
12
|
+
// so a refresh never awaits the recorder and a crash loses at most one batch.
|
|
13
|
+
import { mkdirSync } from 'node:fs';
|
|
14
|
+
import { dirname } from 'node:path';
|
|
15
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
16
|
+
import { withPriority, systemClock, } from '../../data/src/index.js';
|
|
17
|
+
import { ALL_HORIZONS_MASK, HORIZONS_SEC, SCHEMA_SQL, SCHEMA_VERSION } from "./schema.js";
|
|
18
|
+
import { COLUMN_NAMES, fromDsView, fromEnrichedRow, isMaterialChange } from "./snapshot.js";
|
|
19
|
+
import { LABELER_VERSION, labelAtHorizon } from "./labels.js";
|
|
20
|
+
import { dueBounds, selectDue } from "./sweep.js";
|
|
21
|
+
export const DEFAULT_SWEEP = { everyMs: 60_000, maxCallsPerSweep: 6, maxCallsPerDay: 1500, batch: 30 };
|
|
22
|
+
export const DEFAULT_RETENTION = { rawDays: 7, columnsDays: 90, downsampleAfterHours: 24, everyMs: 3_600_000 };
|
|
23
|
+
/** Unchanged rows are re-written this often at most; the horizons are ≥ 1 h, so 30 min keeps every window observed. */
|
|
24
|
+
export const DEFAULT_HEARTBEAT_MS = 1_800_000;
|
|
25
|
+
export const DEFAULT_T0_EVERY_MS = 3_600_000;
|
|
26
|
+
export const DEFAULT_MEMBERS_EVERY_MS = 600_000;
|
|
27
|
+
/** The labeler looks at open (unresolved, window not closed) horizons again after this long. */
|
|
28
|
+
export const LABEL_RETRY_MS = 300_000;
|
|
29
|
+
/** Label job: at most this many t0 rows per pass. */
|
|
30
|
+
export const LABEL_BATCH = 300;
|
|
31
|
+
/** In-memory last-row cache: entries not observed for this long are dropped (re-read from the DB on return). */
|
|
32
|
+
export const SEEN_EVICT_MS = 2 * 3_600_000;
|
|
33
|
+
export class Recorder {
|
|
34
|
+
enabled;
|
|
35
|
+
path;
|
|
36
|
+
db;
|
|
37
|
+
clock;
|
|
38
|
+
focus;
|
|
39
|
+
heartbeatMs;
|
|
40
|
+
changeDetection;
|
|
41
|
+
t0EveryMs;
|
|
42
|
+
membersEveryMs;
|
|
43
|
+
sweepPolicy;
|
|
44
|
+
retention;
|
|
45
|
+
onLog;
|
|
46
|
+
/**
|
|
47
|
+
* Per mint: the last WRITTEN row (columns + hash + time) and the newest observation time this process
|
|
48
|
+
* saw, so unchanged rows cost no DB read. Seeded from the DB the first time a mint shows up in a run.
|
|
49
|
+
*/
|
|
50
|
+
seen = new Map();
|
|
51
|
+
/** Per-feed membership already written: set of mints + time of the last full write. */
|
|
52
|
+
members = new Map();
|
|
53
|
+
lastSweepAt = 0;
|
|
54
|
+
lastLabelAt = 0;
|
|
55
|
+
lastPruneAt = 0;
|
|
56
|
+
sweeping = false;
|
|
57
|
+
closed = false;
|
|
58
|
+
/** Counters for the handoff / --verbose. */
|
|
59
|
+
counters = { observed: 0, written: 0, heartbeat: 0, members: 0, sweepCalls: 0, labelled: 0 };
|
|
60
|
+
stmts;
|
|
61
|
+
constructor(opts = {}) {
|
|
62
|
+
this.enabled = opts.enabled ?? true;
|
|
63
|
+
this.path = opts.path ?? ':memory:';
|
|
64
|
+
this.clock = opts.clock ?? systemClock;
|
|
65
|
+
this.focus = opts.focus ?? (() => []);
|
|
66
|
+
this.heartbeatMs = opts.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
67
|
+
this.changeDetection = opts.changeDetection ?? 'material';
|
|
68
|
+
this.t0EveryMs = opts.t0EveryMs ?? DEFAULT_T0_EVERY_MS;
|
|
69
|
+
this.membersEveryMs = opts.membersEveryMs ?? DEFAULT_MEMBERS_EVERY_MS;
|
|
70
|
+
this.sweepPolicy = { ...DEFAULT_SWEEP, ...opts.sweep };
|
|
71
|
+
this.retention = { ...DEFAULT_RETENTION, ...opts.retention };
|
|
72
|
+
this.onLog = opts.onLog;
|
|
73
|
+
if (this.path !== ':memory:')
|
|
74
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
75
|
+
this.db = new DatabaseSync(this.path);
|
|
76
|
+
this.db.exec('PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 3000; PRAGMA synchronous = NORMAL;');
|
|
77
|
+
this.db.exec(SCHEMA_SQL);
|
|
78
|
+
this.db.prepare('INSERT OR REPLACE INTO recorder_meta (key, value) VALUES (?, ?)').run('schema_version', String(SCHEMA_VERSION));
|
|
79
|
+
const cols = COLUMN_NAMES.join(', ');
|
|
80
|
+
const marks = COLUMN_NAMES.map(() => '?').join(', ');
|
|
81
|
+
this.stmts = {
|
|
82
|
+
insertSnap: this.db.prepare(`INSERT INTO token_snapshots (t, kind, hash, is_t0, horizons_done, next_due_t, focus, sources, raw_json, ${cols}) VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?, ${marks})`),
|
|
83
|
+
getMint: this.db.prepare('SELECT mint, first_t, last_live_t, last_any_t, last_t0_t, last_hash, snapshots FROM mints WHERE mint = ?'),
|
|
84
|
+
upsertMint: this.db.prepare(`INSERT INTO mints (mint, first_t, last_live_t, last_any_t, last_t0_t, last_hash, snapshots) VALUES (?, ?, ?, ?, ?, ?, 1)
|
|
85
|
+
ON CONFLICT(mint) DO UPDATE SET last_live_t = excluded.last_live_t, last_any_t = MAX(last_any_t, excluded.last_any_t), last_t0_t = MAX(last_t0_t, excluded.last_t0_t), last_hash = excluded.last_hash, snapshots = snapshots + 1`),
|
|
86
|
+
bumpAny: this.db.prepare('UPDATE mints SET last_any_t = MAX(last_any_t, ?), snapshots = snapshots + 1 WHERE mint = ?'),
|
|
87
|
+
insertMember: this.db.prepare('INSERT OR IGNORE INTO feed_members (t, feed, rank, mint) VALUES (?, ?, ?, ?)'),
|
|
88
|
+
lastIdentity: this.db.prepare('SELECT symbol, name, launchpad, created_at FROM token_snapshots WHERE mint = ? ORDER BY t DESC LIMIT 1'),
|
|
89
|
+
lastLive: this.db.prepare(`SELECT t, hash, sources, ${cols} FROM token_snapshots WHERE mint = ? AND kind = 'live' ORDER BY t DESC LIMIT 1`),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Attach as the data layer's store listener (and detach any previous one). */
|
|
93
|
+
attach(store) {
|
|
94
|
+
store.listener = this;
|
|
95
|
+
}
|
|
96
|
+
// ---------------------------------------------------------------- observe (StoreListener)
|
|
97
|
+
onEnrich(ev) {
|
|
98
|
+
if (!this.enabled || this.closed)
|
|
99
|
+
return;
|
|
100
|
+
this.observe(ev.rows, this.clock.now());
|
|
101
|
+
}
|
|
102
|
+
onDiscover(ev) {
|
|
103
|
+
if (!this.enabled || this.closed)
|
|
104
|
+
return;
|
|
105
|
+
const now = this.clock.now();
|
|
106
|
+
for (const f of ev.feeds)
|
|
107
|
+
this.recordMembers(f.name, f.mints, now);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Record normalised rows. A row is written when it changed materially against the last row stored
|
|
111
|
+
* for the mint (isMaterialChange; 'exact' mode: any change), when an hourly t0 anchor is due, or
|
|
112
|
+
* (heartbeat) when the stored row is older than `heartbeatMs` so every horizon window holds an
|
|
113
|
+
* observation. Rows whose fetchedAt did not move (served from cache) are skipped after one Map lookup.
|
|
114
|
+
*/
|
|
115
|
+
observe(rows, now = this.clock.now()) {
|
|
116
|
+
const res = { written: 0, skipped: 0, heartbeat: 0, t0: 0 };
|
|
117
|
+
if (!this.enabled || this.closed || rows.length === 0)
|
|
118
|
+
return res;
|
|
119
|
+
const focus = new Set(this.focus());
|
|
120
|
+
const pending = [];
|
|
121
|
+
for (const r of rows) {
|
|
122
|
+
this.counters.observed++;
|
|
123
|
+
if (!r.mint || !r.pair) {
|
|
124
|
+
res.skipped++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const snap = fromEnrichedRow(r);
|
|
128
|
+
const t = snap.t || now;
|
|
129
|
+
let prev = this.seen.get(r.mint);
|
|
130
|
+
if (prev && t <= prev.t) {
|
|
131
|
+
res.skipped++;
|
|
132
|
+
continue;
|
|
133
|
+
} // same fetch as last time: nothing new
|
|
134
|
+
if (!prev) {
|
|
135
|
+
const last = this.lastStored(r.mint);
|
|
136
|
+
if (last) {
|
|
137
|
+
const m = this.stmts.getMint.get(r.mint);
|
|
138
|
+
prev = { ...last, t: 0, t0T: m?.last_t0_t ?? 0 };
|
|
139
|
+
this.seen.set(r.mint, prev);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const unchanged = prev ? (this.changeDetection === 'exact' ? prev.hash === snap.hash : !isMaterialChange(prev.columns, snap.columns)) : false;
|
|
143
|
+
if (prev && unchanged && t - prev.writtenT < this.heartbeatMs && t - prev.t0T < this.t0EveryMs) {
|
|
144
|
+
prev.t = t;
|
|
145
|
+
res.skipped++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (unchanged)
|
|
149
|
+
res.heartbeat++;
|
|
150
|
+
snap.t = t;
|
|
151
|
+
pending.push({ snap, focus: focus.has(r.mint) });
|
|
152
|
+
this.seen.set(r.mint, { columns: snap.columns, hash: snap.hash, t, writtenT: t, t0T: prev?.t0T ?? 0 });
|
|
153
|
+
}
|
|
154
|
+
if (!pending.length)
|
|
155
|
+
return res;
|
|
156
|
+
this.db.exec('BEGIN');
|
|
157
|
+
try {
|
|
158
|
+
for (const p of pending) {
|
|
159
|
+
const isT0 = this.writeSnapshot(p.snap, 'live', p.focus);
|
|
160
|
+
res.written++;
|
|
161
|
+
if (isT0) {
|
|
162
|
+
res.t0++;
|
|
163
|
+
this.seen.get(p.snap.columns.mint).t0T = p.snap.t;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
this.db.exec('COMMIT');
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
this.db.exec('ROLLBACK');
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
this.counters.written += res.written;
|
|
173
|
+
this.counters.heartbeat += res.heartbeat;
|
|
174
|
+
return res;
|
|
175
|
+
}
|
|
176
|
+
/** The mint's last stored live row (one indexed read; only paid the first time a mint is seen this run). */
|
|
177
|
+
lastStored(mint) {
|
|
178
|
+
const row = this.stmts.lastLive.get(mint);
|
|
179
|
+
if (!row)
|
|
180
|
+
return null;
|
|
181
|
+
const columns = {};
|
|
182
|
+
for (const k of COLUMN_NAMES)
|
|
183
|
+
columns[k] = row[k] ?? null;
|
|
184
|
+
let sources = [];
|
|
185
|
+
try {
|
|
186
|
+
sources = JSON.parse(row.sources);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
sources = [];
|
|
190
|
+
}
|
|
191
|
+
columns.sources = sources;
|
|
192
|
+
return { columns: columns, hash: row.hash, writtenT: Number(row.t) };
|
|
193
|
+
}
|
|
194
|
+
/** Insert one row (inside the caller's transaction); returns true when it became a t0 anchor. */
|
|
195
|
+
writeSnapshot(snap, kind, focus) {
|
|
196
|
+
const c = snap.columns;
|
|
197
|
+
const m = this.stmts.getMint.get(c.mint);
|
|
198
|
+
const isT0 = kind === 'live' && (!m || snap.t - m.last_t0_t >= this.t0EveryMs);
|
|
199
|
+
const nextDue = isT0 ? snap.t + HORIZONS_SEC[0] * 1000 : 0;
|
|
200
|
+
const values = COLUMN_NAMES.map((k) => c[k]);
|
|
201
|
+
this.stmts.insertSnap.run(snap.t, kind, snap.hash, isT0 ? 1 : 0, nextDue, focus ? 1 : 0, JSON.stringify(c.sources), focus ? JSON.stringify(snap.raw) : null, ...values);
|
|
202
|
+
if (kind === 'live')
|
|
203
|
+
this.stmts.upsertMint.run(c.mint, snap.t, snap.t, snap.t, isT0 ? snap.t : 0, snap.hash);
|
|
204
|
+
else if (m)
|
|
205
|
+
this.stmts.bumpAny.run(snap.t, c.mint);
|
|
206
|
+
else
|
|
207
|
+
this.stmts.upsertMint.run(c.mint, snap.t, 0, snap.t, 0, snap.hash);
|
|
208
|
+
return isT0;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Feed membership: entrants (mints not in the feed's last written list) are written at once with their
|
|
212
|
+
* rank; the whole list is rewritten every `membersEveryMs` so ranks have a time series without a row
|
|
213
|
+
* per mint per refresh (10 sub-feeds x 100 rows x 6/min would be 8.6 M rows a day).
|
|
214
|
+
*/
|
|
215
|
+
recordMembers(feed, mints, now = this.clock.now()) {
|
|
216
|
+
if (!this.enabled || this.closed)
|
|
217
|
+
return 0;
|
|
218
|
+
const prev = this.members.get(feed);
|
|
219
|
+
const full = !prev || now - prev.fullAt >= this.membersEveryMs;
|
|
220
|
+
let n = 0;
|
|
221
|
+
this.db.exec('BEGIN');
|
|
222
|
+
try {
|
|
223
|
+
mints.forEach((mint, rank) => {
|
|
224
|
+
if (full || !prev.mints.has(mint)) {
|
|
225
|
+
this.stmts.insertMember.run(now, feed, rank, mint);
|
|
226
|
+
n++;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
this.db.exec('COMMIT');
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
this.db.exec('ROLLBACK');
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
this.members.set(feed, { mints: new Set(mints), fullAt: full ? now : prev.fullAt });
|
|
236
|
+
this.counters.members += n;
|
|
237
|
+
return n;
|
|
238
|
+
}
|
|
239
|
+
// ---------------------------------------------------------------- maintenance (sweep + label + prune)
|
|
240
|
+
/**
|
|
241
|
+
* The app calls this from its tick (every 60 s is plenty). Runs whichever job is due; never throws
|
|
242
|
+
* (errors are logged and counted). Sweeps are the only network activity and go through `deps`.
|
|
243
|
+
*/
|
|
244
|
+
async maintain(deps, now = this.clock.now()) {
|
|
245
|
+
const out = { sweep: null, labels: null, prune: null };
|
|
246
|
+
if (!this.enabled || this.closed)
|
|
247
|
+
return out;
|
|
248
|
+
// Memory: forget mints not observed for a while (re-seeded from the DB when they come back), so a
|
|
249
|
+
// day-long session over thousands of churned mints does not keep every last row on the heap.
|
|
250
|
+
for (const [mint, s] of this.seen)
|
|
251
|
+
if (now - s.t > SEEN_EVICT_MS)
|
|
252
|
+
this.seen.delete(mint);
|
|
253
|
+
try {
|
|
254
|
+
if (now - this.lastSweepAt >= this.sweepPolicy.everyMs)
|
|
255
|
+
out.sweep = await this.sweep(deps, now);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
this.log(`sweep failed: ${msg(err)}`);
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
if (now - this.lastLabelAt >= this.sweepPolicy.everyMs) {
|
|
262
|
+
this.lastLabelAt = now;
|
|
263
|
+
out.labels = this.labelPending(now);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
this.log(`label failed: ${msg(err)}`);
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
if (now - this.lastPruneAt >= this.retention.everyMs) {
|
|
271
|
+
this.lastPruneAt = now;
|
|
272
|
+
out.prune = this.prune(now);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
this.log(`prune failed: ${msg(err)}`);
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
/** Mints whose horizon window is open and unobserved (sweep.ts), soonest-closing first. */
|
|
281
|
+
due(now = this.clock.now(), max = this.sweepPolicy.maxCallsPerSweep * this.sweepPolicy.batch) {
|
|
282
|
+
const b = dueBounds(now);
|
|
283
|
+
const rows = this.db.prepare('SELECT mint, last_live_t, last_any_t FROM mints WHERE last_live_t >= ? AND last_live_t <= ? AND last_live_t > 0').all(b.from, b.to);
|
|
284
|
+
return selectDue(rows, now, max);
|
|
285
|
+
}
|
|
286
|
+
/** Sweep calls issued in the current UTC day (from the sweeps ledger). */
|
|
287
|
+
sweepCallsToday(now = this.clock.now()) {
|
|
288
|
+
const day = new Date(now).toISOString().slice(0, 10);
|
|
289
|
+
const start = Date.parse(`${day}T00:00:00Z`);
|
|
290
|
+
const r = this.db.prepare('SELECT COALESCE(SUM(calls), 0) AS n FROM sweeps WHERE started_at >= ?').get(start);
|
|
291
|
+
return Number(r.n);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* One outcome sweep: the due mints, `batch` per DexScreener call, at most `maxCallsPerSweep` calls,
|
|
295
|
+
* each issued at 'low' priority inside the data layer's own limiter and health gate (a paused bucket
|
|
296
|
+
* after a 429 or a down source means the sweep waits for the next tick). Every answered mint gets a
|
|
297
|
+
* `kind = 'sweep'` row; a mint DexScreener did not answer for gets a `src_market = 'none'` row
|
|
298
|
+
* (pair vanished / never had a pool), which the labeler treats as the prototype did.
|
|
299
|
+
*/
|
|
300
|
+
async sweep(deps, now = this.clock.now()) {
|
|
301
|
+
const none = (skipped) => ({ mints: 0, calls: 0, answered: 0, skipped });
|
|
302
|
+
if (!this.enabled || this.closed)
|
|
303
|
+
return none('disabled');
|
|
304
|
+
if (this.sweeping)
|
|
305
|
+
return none('in-flight');
|
|
306
|
+
const paused = deps.limiter.bucket('dexscreener').stats().pausedUntil;
|
|
307
|
+
if (paused > now)
|
|
308
|
+
return none('paused');
|
|
309
|
+
const todays = this.sweepCallsToday(now);
|
|
310
|
+
const budget = Math.min(this.sweepPolicy.maxCallsPerSweep, this.sweepPolicy.maxCallsPerDay - todays);
|
|
311
|
+
if (budget <= 0)
|
|
312
|
+
return none('day-cap');
|
|
313
|
+
const due = this.due(now, budget * this.sweepPolicy.batch);
|
|
314
|
+
this.lastSweepAt = now;
|
|
315
|
+
if (!due.length)
|
|
316
|
+
return none('not-due');
|
|
317
|
+
this.sweeping = true;
|
|
318
|
+
const started = now;
|
|
319
|
+
const ins = this.db.prepare('INSERT INTO sweeps (started_at, mints, calls) VALUES (?, ?, 0)').run(started, due.length);
|
|
320
|
+
const sweepId = Number(ins.lastInsertRowid);
|
|
321
|
+
let calls = 0;
|
|
322
|
+
let answered = 0;
|
|
323
|
+
let error;
|
|
324
|
+
try {
|
|
325
|
+
for (let i = 0; i < due.length; i += this.sweepPolicy.batch) {
|
|
326
|
+
const chunk = due.slice(i, i + this.sweepPolicy.batch).map((d) => d.mint);
|
|
327
|
+
calls++;
|
|
328
|
+
const views = await withPriority('low', () => deps.dexscreener.tokens(chunk));
|
|
329
|
+
const t = this.clock.now();
|
|
330
|
+
this.db.exec('BEGIN');
|
|
331
|
+
try {
|
|
332
|
+
for (const mint of chunk) {
|
|
333
|
+
const v = views.get(mint);
|
|
334
|
+
if (v)
|
|
335
|
+
answered++;
|
|
336
|
+
const base = this.stmts.lastIdentity.get(mint);
|
|
337
|
+
this.writeSnapshot(fromDsView(mint, v, base, t), 'sweep', false);
|
|
338
|
+
}
|
|
339
|
+
this.db.exec('COMMIT');
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
this.db.exec('ROLLBACK');
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
this.db.prepare('UPDATE sweeps SET calls = ?, answered = ? WHERE sweep_id = ?').run(calls, answered, sweepId);
|
|
346
|
+
if (this.closed)
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
error = msg(err);
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
this.sweeping = false;
|
|
355
|
+
this.db.prepare('UPDATE sweeps SET finished_at = ?, calls = ?, answered = ?, error = ? WHERE sweep_id = ?').run(this.clock.now(), calls, answered, error ?? null, sweepId);
|
|
356
|
+
this.counters.sweepCalls += calls;
|
|
357
|
+
}
|
|
358
|
+
this.log(`sweep: ${due.length} mints, ${calls} calls, ${answered} answered${error ? `, error ${error}` : ''}`);
|
|
359
|
+
return { mints: due.length, calls, answered, skipped: null, ...(error ? { error } : {}) };
|
|
360
|
+
}
|
|
361
|
+
// ---------------------------------------------------------------- labels
|
|
362
|
+
/**
|
|
363
|
+
* Label every t0 anchor whose next horizon is due. For (t0, h): t1 = the first observation at or
|
|
364
|
+
* after t0 + h within t0 + 2h; none yet and the window still open → look again in LABEL_RETRY_MS;
|
|
365
|
+
* none and the window closed → `unobserved`. max_mcap_ratio uses every observation in (t0, t1].
|
|
366
|
+
*/
|
|
367
|
+
labelPending(now = this.clock.now(), max = LABEL_BATCH) {
|
|
368
|
+
const res = { labelled: 0, pending: 0, counts: {} };
|
|
369
|
+
if (!this.enabled || this.closed)
|
|
370
|
+
return res;
|
|
371
|
+
const t0s = this.db.prepare(`SELECT snap_id, mint, t, horizons_done, mcap, liquidity_usd, price_usd, vol_h1, pair_address, src_market, launchpad
|
|
372
|
+
FROM token_snapshots WHERE is_t0 = 1 AND horizons_done < ${ALL_HORIZONS_MASK} AND next_due_t <= ? ORDER BY next_due_t LIMIT ?`).all(now, max);
|
|
373
|
+
if (!t0s.length)
|
|
374
|
+
return res;
|
|
375
|
+
const obsStmt = this.db.prepare('SELECT mint, t, mcap, liquidity_usd, price_usd, vol_h1, pair_address, src_market, launchpad FROM token_snapshots WHERE mint = ? AND t > ? AND t <= ? ORDER BY t');
|
|
376
|
+
const insOutcome = this.db.prepare('INSERT OR REPLACE INTO outcomes (mint, t0, horizon_sec, t1, label, label_tier, mcap_ratio, liq_ratio, price_ratio, max_mcap_ratio, evidence, labeler_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
377
|
+
const upd = this.db.prepare('UPDATE token_snapshots SET horizons_done = ?, next_due_t = ? WHERE snap_id = ?');
|
|
378
|
+
this.db.exec('BEGIN');
|
|
379
|
+
try {
|
|
380
|
+
for (const t0 of t0s) {
|
|
381
|
+
let done = t0.horizons_done;
|
|
382
|
+
let nextDue = Number.POSITIVE_INFINITY;
|
|
383
|
+
// One read for the widest pending window; slice per horizon in memory.
|
|
384
|
+
const maxH = Math.max(...HORIZONS_SEC.filter((_, i) => !(done & (1 << i))));
|
|
385
|
+
const obs = obsStmt.all(t0.mint, t0.t, t0.t + 2 * maxH * 1000);
|
|
386
|
+
HORIZONS_SEC.forEach((h, i) => {
|
|
387
|
+
const bit = 1 << i;
|
|
388
|
+
if (done & bit)
|
|
389
|
+
return;
|
|
390
|
+
const open = t0.t + h * 1000;
|
|
391
|
+
const close = t0.t + 2 * h * 1000;
|
|
392
|
+
if (now < open) {
|
|
393
|
+
nextDue = Math.min(nextDue, open);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const idx = obs.findIndex((o) => o.t >= open && o.t <= close);
|
|
397
|
+
let row;
|
|
398
|
+
if (idx >= 0)
|
|
399
|
+
row = labelAtHorizon(t0, obs.slice(0, idx + 1), h);
|
|
400
|
+
else if (now > close)
|
|
401
|
+
row = labelAtHorizon(t0, [], h);
|
|
402
|
+
else {
|
|
403
|
+
nextDue = Math.min(nextDue, now + LABEL_RETRY_MS);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
insOutcome.run(row.mint, row.t0, row.horizon_sec, row.t1, row.label, row.label_tier, row.mcap_ratio, row.liq_ratio, row.price_ratio, row.max_mcap_ratio, row.evidence, row.labeler_version);
|
|
407
|
+
done |= bit;
|
|
408
|
+
res.labelled++;
|
|
409
|
+
res.counts[row.label] = (res.counts[row.label] ?? 0) + 1;
|
|
410
|
+
});
|
|
411
|
+
if (done !== t0.horizons_done || Number.isFinite(nextDue))
|
|
412
|
+
upd.run(done, done === ALL_HORIZONS_MASK ? 0 : Number.isFinite(nextDue) ? nextDue : now + LABEL_RETRY_MS, t0.snap_id);
|
|
413
|
+
if (done !== ALL_HORIZONS_MASK)
|
|
414
|
+
res.pending++;
|
|
415
|
+
}
|
|
416
|
+
this.db.exec('COMMIT');
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
this.db.exec('ROLLBACK');
|
|
420
|
+
throw err;
|
|
421
|
+
}
|
|
422
|
+
this.counters.labelled += res.labelled;
|
|
423
|
+
if (res.labelled)
|
|
424
|
+
this.log(`labels: ${res.labelled} written (${Object.entries(res.counts).map(([k, v]) => `${k} ${v}`).join(', ')}), ${res.pending} t0 rows still open`);
|
|
425
|
+
return res;
|
|
426
|
+
}
|
|
427
|
+
// ---------------------------------------------------------------- retention
|
|
428
|
+
/**
|
|
429
|
+
* Retention (design §4.1): raw JSON after `rawDays`; after `downsampleAfterHours` keep one live row per
|
|
430
|
+
* mint per minute (the first), never a t0 anchor or a row an outcome cites as t1; everything older
|
|
431
|
+
* than `columnsDays` goes (outcomes and verdicts stay forever). Downsampling is incremental: a
|
|
432
|
+
* watermark in recorder_meta means each pass only looks at the hours that newly crossed the line.
|
|
433
|
+
*/
|
|
434
|
+
prune(now = this.clock.now()) {
|
|
435
|
+
const res = { rawCleared: 0, downsampled: 0, expired: 0, membersExpired: 0 };
|
|
436
|
+
if (this.closed)
|
|
437
|
+
return res;
|
|
438
|
+
const day = 86_400_000;
|
|
439
|
+
const rawBefore = now - this.retention.rawDays * day;
|
|
440
|
+
const colsBefore = now - this.retention.columnsDays * day;
|
|
441
|
+
const dsBefore = now - this.retention.downsampleAfterHours * 3_600_000;
|
|
442
|
+
const wm = this.db.prepare("SELECT value FROM recorder_meta WHERE key = 'downsampled_until'").get();
|
|
443
|
+
const from = Math.max(wm ? Number(wm.value) : 0, colsBefore);
|
|
444
|
+
this.db.exec('BEGIN');
|
|
445
|
+
try {
|
|
446
|
+
res.rawCleared = Number(this.db.prepare('UPDATE token_snapshots SET raw_json = NULL WHERE raw_json IS NOT NULL AND t < ?').run(rawBefore).changes);
|
|
447
|
+
if (dsBefore > from) {
|
|
448
|
+
res.downsampled = Number(this.db.prepare(`DELETE FROM token_snapshots WHERE snap_id IN (
|
|
449
|
+
SELECT s.snap_id FROM token_snapshots s
|
|
450
|
+
WHERE s.t >= ? AND s.t < ? AND s.is_t0 = 0 AND s.kind = 'live'
|
|
451
|
+
AND s.snap_id > (SELECT MIN(m.snap_id) FROM token_snapshots m WHERE m.mint = s.mint AND m.t >= (s.t / 60000) * 60000 AND m.t < (s.t / 60000) * 60000 + 60000)
|
|
452
|
+
AND NOT EXISTS (SELECT 1 FROM outcomes o WHERE o.mint = s.mint AND o.t1 = s.t)
|
|
453
|
+
)`).run(from, dsBefore).changes);
|
|
454
|
+
this.db.prepare("INSERT OR REPLACE INTO recorder_meta (key, value) VALUES ('downsampled_until', ?)").run(String(dsBefore));
|
|
455
|
+
}
|
|
456
|
+
res.expired = Number(this.db.prepare('DELETE FROM token_snapshots WHERE t < ?').run(colsBefore).changes);
|
|
457
|
+
res.membersExpired = Number(this.db.prepare('DELETE FROM feed_members WHERE t < ?').run(colsBefore).changes);
|
|
458
|
+
this.db.prepare('DELETE FROM mints WHERE last_any_t < ?').run(colsBefore);
|
|
459
|
+
this.db.prepare('DELETE FROM sweeps WHERE started_at < ?').run(colsBefore);
|
|
460
|
+
this.db.exec('COMMIT');
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
this.db.exec('ROLLBACK');
|
|
464
|
+
throw err;
|
|
465
|
+
}
|
|
466
|
+
if (res.rawCleared || res.downsampled || res.expired)
|
|
467
|
+
this.log(`prune: raw ${res.rawCleared}, downsampled ${res.downsampled}, expired ${res.expired} rows`);
|
|
468
|
+
return res;
|
|
469
|
+
}
|
|
470
|
+
// ---------------------------------------------------------------- read side
|
|
471
|
+
stats(now = this.clock.now()) {
|
|
472
|
+
const one = (sql, ...args) => this.db.prepare(sql).get(...args);
|
|
473
|
+
const s = one("SELECT COUNT(*) AS n, SUM(kind = 'live') AS live, SUM(kind = 'sweep') AS sweep, SUM(is_t0) AS t0, SUM(raw_json IS NOT NULL) AS raw, MIN(t) AS oldest, MAX(t) AS newest FROM token_snapshots");
|
|
474
|
+
const byLabel = {};
|
|
475
|
+
for (const r of this.db.prepare('SELECT label, COUNT(*) AS n FROM outcomes GROUP BY label').all())
|
|
476
|
+
byLabel[r.label] = Number(r.n);
|
|
477
|
+
let dbBytes = null;
|
|
478
|
+
try {
|
|
479
|
+
const pc = one('PRAGMA page_count').page_count;
|
|
480
|
+
const ps = one('PRAGMA page_size').page_size;
|
|
481
|
+
dbBytes = Number(pc) * Number(ps);
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
dbBytes = null;
|
|
485
|
+
}
|
|
486
|
+
let bytesPerSnapshot = null;
|
|
487
|
+
let recorderBytes = null;
|
|
488
|
+
try {
|
|
489
|
+
const d = one("SELECT SUM(pgsize) AS b FROM dbstat WHERE name IN ('token_snapshots', 'token_snapshots_mint_t', 'token_snapshots_t', 'token_snapshots_t0_pending')");
|
|
490
|
+
if (d.b !== null && Number(s.n) > 0)
|
|
491
|
+
bytesPerSnapshot = Number(d.b) / Number(s.n);
|
|
492
|
+
const all = one("SELECT SUM(pgsize) AS b FROM dbstat WHERE name IN ('recorder_meta', 'feed_members', 'feed_members_mint_t', 'token_snapshots', 'token_snapshots_mint_t', 'token_snapshots_t', 'token_snapshots_t0_pending', 'mints', 'mints_last_live', 'sweeps', 'chain_events', 'chain_events_mint_t', 'outcomes', 'outcomes_label', 'verdicts', 'verdicts_mint_t')");
|
|
493
|
+
recorderBytes = all.b === null ? null : Number(all.b);
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// dbstat not compiled in: estimate from the stored widths (rows + the three indexes).
|
|
497
|
+
if (Number(s.n) > 0) {
|
|
498
|
+
const w = one("SELECT AVG(LENGTH(COALESCE(symbol,'')) + LENGTH(COALESCE(name,'')) + LENGTH(COALESCE(pair_address,'')) + LENGTH(COALESCE(raw_json,'')) + LENGTH(sources) + 44 + 8 * 48) AS w FROM token_snapshots").w;
|
|
499
|
+
bytesPerSnapshot = Number(w) + 2 * 60;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
enabled: this.enabled,
|
|
504
|
+
schemaVersion: SCHEMA_VERSION,
|
|
505
|
+
labelerVersion: LABELER_VERSION,
|
|
506
|
+
snapshots: Number(s.n), live: Number(s.live ?? 0), sweep: Number(s.sweep ?? 0), t0: Number(s.t0 ?? 0), withRaw: Number(s.raw ?? 0),
|
|
507
|
+
mints: Number(one('SELECT COUNT(*) AS n FROM mints').n),
|
|
508
|
+
feedMembers: Number(one('SELECT COUNT(*) AS n FROM feed_members').n),
|
|
509
|
+
outcomes: Number(one('SELECT COUNT(*) AS n FROM outcomes').n),
|
|
510
|
+
outcomesByLabel: byLabel,
|
|
511
|
+
sweeps: Number(one('SELECT COUNT(*) AS n FROM sweeps').n),
|
|
512
|
+
sweepCallsToday: this.sweepCallsToday(now),
|
|
513
|
+
dbBytes, recorderBytes, bytesPerSnapshot,
|
|
514
|
+
oldestT: s.oldest === null ? null : Number(s.oldest),
|
|
515
|
+
newestT: s.newest === null ? null : Number(s.newest),
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
/** Raw access for the export / backtest readers (read-only use). */
|
|
519
|
+
get database() {
|
|
520
|
+
return this.db;
|
|
521
|
+
}
|
|
522
|
+
/** Mints currently held in the in-memory last-row cache (diagnostics / tests). */
|
|
523
|
+
get cachedMints() {
|
|
524
|
+
return this.seen.size;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Close the DB. First (task-1361) label whatever t0 anchors are due: code only, one bounded synchronous
|
|
528
|
+
* pass, so a run shorter than the first maintenance tick still leaves its outcomes behind instead of
|
|
529
|
+
* nothing. Never throws; a failure is logged and the DB still closes.
|
|
530
|
+
*/
|
|
531
|
+
close() {
|
|
532
|
+
if (this.closed)
|
|
533
|
+
return { labelled: 0 };
|
|
534
|
+
let labelled = 0;
|
|
535
|
+
if (this.enabled) {
|
|
536
|
+
try {
|
|
537
|
+
labelled = this.labelPending(this.clock.now()).labelled;
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
this.log(`labels at close failed: ${msg(err)}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
this.closed = true;
|
|
544
|
+
this.db.close();
|
|
545
|
+
return { labelled };
|
|
546
|
+
}
|
|
547
|
+
log(line) {
|
|
548
|
+
this.onLog?.(`recorder: ${line}\n`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function msg(err) {
|
|
552
|
+
return err instanceof Error ? err.message : String(err);
|
|
553
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// When the app lets the recorder run maintenance (task-1361).
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: before task-1361 the interactive loop started its maintenance timer at launch with a
|
|
4
|
+
// full RECORDER_TICK_MS (60 s) of silence, and nothing ran at quit. Every real run of the app up to
|
|
5
|
+
// 2026-10-12 had been a pty smoke / capture run of 8–14 s (reconstructed from the snapshot timestamps in
|
|
6
|
+
// ~/.local/share/dexterm/dexterm.db), so in two days of recording — 1676 t0 anchors, 762 mints — not one
|
|
7
|
+
// sweep ran and not one outcome was labelled, and both backtests only ever saw fixtures. The jobs
|
|
8
|
+
// themselves were fine: the first `labelPending()` over that DB wrote 1106 outcomes.
|
|
9
|
+
//
|
|
10
|
+
// The rule now: the first maintenance pass runs RECORDER_FIRST_TICK_MS after launch (after the first
|
|
11
|
+
// paint, before a short run ends), then every RECORDER_TICK_MS; and `Recorder.close()` labels whatever
|
|
12
|
+
// is due (code only, no network) so even a run shorter than the first tick leaves its outcomes behind.
|
|
13
|
+
/** How often the app lets the recorder run maintenance (sweep / label / prune checks); each job has its own spacing. */
|
|
14
|
+
export const RECORDER_TICK_MS = 60_000;
|
|
15
|
+
/** The first maintenance pass after launch: late enough to be after the first paint, early enough for a short run. */
|
|
16
|
+
export const RECORDER_FIRST_TICK_MS = 5_000;
|
|
17
|
+
/** Decides, from the app's tick, when `Recorder.maintain()` is called. Pure over the clock values it is given. */
|
|
18
|
+
export class MaintenanceSchedule {
|
|
19
|
+
lastAt = null;
|
|
20
|
+
launchedAt;
|
|
21
|
+
firstAfterMs;
|
|
22
|
+
everyMs;
|
|
23
|
+
constructor(launchedAt, firstAfterMs = RECORDER_FIRST_TICK_MS, everyMs = RECORDER_TICK_MS) {
|
|
24
|
+
this.launchedAt = launchedAt;
|
|
25
|
+
this.firstAfterMs = firstAfterMs;
|
|
26
|
+
this.everyMs = everyMs;
|
|
27
|
+
}
|
|
28
|
+
/** True (once per window) when a maintenance pass is due at `now`; records the pass. */
|
|
29
|
+
due(now) {
|
|
30
|
+
const ok = this.lastAt === null ? now - this.launchedAt >= this.firstAfterMs : now - this.lastAt >= this.everyMs;
|
|
31
|
+
if (ok)
|
|
32
|
+
this.lastAt = now;
|
|
33
|
+
return ok;
|
|
34
|
+
}
|
|
35
|
+
/** Time of the last pass this schedule granted (null before the first). */
|
|
36
|
+
get lastRunAt() {
|
|
37
|
+
return this.lastAt;
|
|
38
|
+
}
|
|
39
|
+
/** When the next pass becomes due. */
|
|
40
|
+
nextDueAt() {
|
|
41
|
+
return this.lastAt === null ? this.launchedAt + this.firstAfterMs : this.lastAt + this.everyMs;
|
|
42
|
+
}
|
|
43
|
+
}
|